| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- /** Isolated browser acceptance fixture. Never loaded by the production profile. */
- import * as fs from 'node:fs'
- import * as path from 'node:path'
- import { LlmAdapter, createUserMessage } from '@deepseek-ai/dsh-llm'
- import { attachIndexFixture } from './index-browser-extension.mjs'
- import { seedGraphFixture } from './graph-data.mjs'
- export const name = 'webnovel-browser-acceptance'
- export const inject = ['agents', 'agentLoop', 'sessionPersistence', 'workspaceRegistry', 'llm', 'sessionController', 'webServer', ...(process.env.WEBNOVEL_INDEX_CHECK ? ['embeddings', 'settings', 'credentials'] : [])]
- export async function apply(ctx) {
- const root = process.env.WEBNOVEL_BROWSER_WORKSPACE
- const reportPath = process.env.WEBNOVEL_BROWSER_REPORT
- if (!root || !path.isAbsolute(root) || !reportPath) throw new Error('Missing isolated browser acceptance paths')
- const put = (relative, text) => {
- const target = path.join(root, relative)
- if (fs.existsSync(target)) return
- fs.mkdirSync(path.dirname(target), { recursive: true })
- fs.writeFileSync(target, text)
- }
- const longText = '林舟在渡口停下脚步。潮声越过石阶,信封上的字迹依然清晰。\n\n'.repeat(35)
- for (const [book, id] of [['验收作品甲', 'acceptance-a'], ['验收作品乙', 'acceptance-b']]) {
- put(`${book}/作品契约/契约.md`, `---\n书id: ${id}\n版本: 1\n---\n# ${book}\n\n浏览器验收临时作品。\n`)
- put(`${book}/大纲/故事骨架.md`, '---\n版本: 1\n---\n# 故事骨架\n\n一封来信改变了渡口的日常。\n')
- put(`${book}/草稿区/草稿/卷01-来信/稿1.md`, `---\n身份:\n 卷: 1\n 章: 1\n 章名: 来信\n版本: 1\n父版本: null\n角色: 待审稿\n选定: true\n---\n# 来信\n\n${longText}`)
- put(`${book}/定稿/卷01/0002-归航.md`, '---\n身份:\n 卷: 1\n 章: 2\n 章名: 归航\n角色: 已定稿\n版本: 1\n---\n# 归航\n\n已定稿的原文,只读验证。\n')
- }
- seedGraphFixture(path.join(root, '验收作品甲'))
- put('书房/知识库/写作笔记.md', '# 写作笔记\n\n共享资料的原始内容。\n')
- put('书房/作者记忆/偏好.md', '# 作者偏好\n\n保留作者自己的句子。\n')
- const report = { requests: 0, savesReceived: 0, indexErrorsReceived: 0, complete: 0, customEvents: 0 }
- const persist = () => fs.writeFileSync(reportPath, JSON.stringify(report, null, 2))
- if (process.env.WEBNOVEL_INDEX_CHECK) await attachIndexFixture(ctx, root, report, persist)
- class AcceptanceAdapter extends LlmAdapter {
- providerInfo(provider) { return { id: provider, name: '本地验收提供方' } }
- async listModels() { return [{ id: 'fixture', name: '本地验收' }] }
- async * stream(options) {
- report.requests++
- if (options.system?.includes('你为小说原文识别连续场景')) {
- const paragraphs = JSON.parse(options.messages[0].content[0].text)
- const text = JSON.stringify({ ends: [paragraphs.length] })
- report.sceneRequests = (report.sceneRequests ?? 0) + 1
- yield { type: 'block-start', index: 0, blockType: 'text' }
- yield { type: 'text-delta', index: 0, text }
- yield { type: 'block-end', index: 0, block: { type: 'text', text } }
- yield { type: 'finish', reason: { kind: 'stop' } }
- persist()
- return
- }
- const messages = JSON.stringify(options.messages)
- if (messages.includes('作者通过书房编辑器保存了文档')) report.savesReceived++
- if (messages.includes('后台检索索引需要处理')) report.indexErrorsReceived++
- const document = new URL(`http://127.0.0.1:${ctx.webServer.port}/`)
- document.searchParams.set('webnovel', JSON.stringify({ sessionId: 'webnovel-browser-acceptance', ref: { space: 'book:acceptance-a', path: '草稿区/草稿/卷01-来信/稿1.md' } }))
- const text = '浏览器验收会话。\n\n[打开原文](<' + document + '>)\n\n保存通知已进入本次原生对话。'
- yield { type: 'block-start', index: 0, blockType: 'text' }
- yield { type: 'text-delta', index: 0, text }
- yield { type: 'block-end', index: 0, block: { type: 'text', text } }
- yield { type: 'usage', usage: { inputTokens: 0, outputTokens: 0 } }
- yield { type: 'finish', reason: { kind: 'stop' } }
- persist()
- }
- }
- ctx.llm.registerAdapter(['webnovel-acceptance'], new AcceptanceAdapter())
- const workspace = await ctx.workspaceRegistry.create(root, 'S5 临时验收书房')
- const sessionId = 'webnovel-browser-acceptance'
- const exists = (await ctx.sessionPersistence.list()).some(snapshot => snapshot.header.id === sessionId)
- const options = { provider: 'webnovel-acceptance', model: 'fixture' }
- const handle = exists ? await ctx.agents.resume({ resumeSessionId: sessionId, agentOptions: options })
- : await ctx.agents.create({ sessionId, meta: { cwd: root, title: 'S5 书房验收' }, agentOptions: options })
- const off = ctx.on('agent/status', ({ agent, status }) => {
- if (agent !== handle.agent || status !== 'idle') return
- report.complete++
- report.customEvents = Array.from({ length: agent.session.seq }, (_, index) => agent.session.eventAt(index)).filter(event => event.type.startsWith('novel/')).length
- persist()
- })
- if (!exists) await new Promise((resolve, reject) => {
- const timer = setTimeout(() => { done(); reject(new Error('Acceptance session did not finish')) }, 10000)
- const done = ctx.on('agent/status', ({ agent, status }) => {
- if (agent !== handle.agent || status !== 'idle') return
- clearTimeout(timer); done(); resolve()
- })
- handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: '打开验收作品,验证文档链接。' }], source: { kind: 'user' } }))
- })
- await ctx.sessionPersistence.flush()
- await workspace.attachSession(sessionId)
- await ctx.sessionController.rename({ sessionId, title: process.env.WEBNOVEL_INDEX_CHECK ? 'S6 后台索引验收' : 'S5 书房验收' })
- const secondRoot = path.join(root, '另一工作范围')
- fs.mkdirSync(secondRoot, { recursive: true })
- const secondWorkspace = await ctx.workspaceRegistry.create(secondRoot, 'S5 空工作范围')
- const secondId = 'webnovel-browser-second'
- const secondExists = (await ctx.sessionPersistence.list()).some(snapshot => snapshot.header.id === secondId)
- const second = secondExists ? await ctx.agents.resume({ resumeSessionId: secondId, agentOptions: options })
- : await ctx.agents.create({ sessionId: secondId, meta: { cwd: secondRoot, title: 'S5 会话隔离验收' }, agentOptions: options })
- await ctx.sessionPersistence.flush()
- await secondWorkspace.attachSession(secondId)
- await ctx.sessionController.rename({ sessionId: secondId, title: 'S5 会话隔离验收' })
- persist()
- ctx.on('dispose', () => { off(); return Promise.all([handle.dispose(), second.dispose()]) })
- }
|