packaging-probe.mjs 3.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. /** Copy this probe into an isolated profile; it imports no workspace code. */
  2. import assert from 'node:assert/strict'
  3. import fs from 'node:fs'
  4. import path from 'node:path'
  5. import { createRequire } from 'node:module'
  6. export const name = 'scriptor-package-acceptance'
  7. export const inject = ['agents', 'agentLoop', 'workspaceRegistry', 'skills', 'tools', 'sessionPersistence', 'sessionController']
  8. export async function apply(ctx, config) {
  9. const report = { installed: config.installed, ok: false }
  10. const progress = step => { report.step = step; fs.writeFileSync(config.report, JSON.stringify(report, null, 2) + '\n') }
  11. try {
  12. progress('workspace')
  13. const registry = await ctx.workspaceRegistry.create(config.workspace, '插件安装验收')
  14. progress('agent')
  15. const sessionId = `packaging-${config.stage}`
  16. const exists = (await ctx.sessionPersistence.list()).some(item => item.header.id === sessionId)
  17. const handle = exists ? await ctx.agents.resume({ resumeSessionId: sessionId })
  18. : await ctx.agents.create({ sessionId, meta: { cwd: config.workspace, title: '插件安装验收' } })
  19. ctx.on('dispose', () => handle.dispose())
  20. const agent = handle.agent
  21. progress('tools and skills')
  22. const tools = ctx.tools.schemas(agent).filter(tool => tool.name.startsWith('novel_'))
  23. assert.equal(tools.length, config.installed ? 27 : 0)
  24. const skills = (await ctx.skills.list({ cwd: config.workspace, scope: agent })).filter(skill => skill.provider === 'webnovel-bundled')
  25. assert.equal(skills.length, config.installed ? 10 : 0)
  26. report.tools = tools.length
  27. report.skills = []
  28. if (config.installed) {
  29. progress('module identity')
  30. const hostRequire = createRequire(config.hostAnchor)
  31. const packedRequire = createRequire(hostRequire.resolve('@linfengqaqtat/dsh-scriptor', { paths: [config.profile] }))
  32. report.peers = {}
  33. // React is supplied by the browser module loader, not Node's Host resolver.
  34. for (const peer of ['@deepseek-ai/cordis', '@deepseek-ai/dsh-tools', '@deepseek-ai/dsh-llm', '@deepseek-ai/dsh-skill-filesystem']) {
  35. const installed = fs.realpathSync(packedRequire.resolve(peer))
  36. assert.equal(installed, fs.realpathSync(hostRequire.resolve(peer)), `Duplicate shared runtime: ${peer}`)
  37. report.peers[peer] = 'same host module'
  38. }
  39. const packageRoot = path.dirname(packedRequire.resolve('@linfengqaqtat/dsh-scriptor/package.json'))
  40. for (const summary of skills) {
  41. const skill = await ctx.skills.get(summary.name, { cwd: config.workspace, scope: agent })
  42. assert.ok(skill.content.length > 100)
  43. assert.ok(fs.realpathSync(skill.resourceBase.path).startsWith(fs.realpathSync(path.join(packageRoot, 'skills'))))
  44. report.skills.push({ name: skill.name, resource: path.relative(packageRoot, skill.resourceBase.path).replaceAll('\\', '/'), chars: skill.content.length })
  45. }
  46. const result = await ctx.tools.execute({ agent, name: 'novel_select_book', arguments: { bookId: 'packaging-book' }, signal: new AbortController().signal, callId: 'packaging-select' })
  47. assert.equal(result.value.ok, true, JSON.stringify(result))
  48. report.selectBook = true
  49. }
  50. progress('source isolation')
  51. if (config.blockedSource) {
  52. assert.equal(process.permission?.has('fs.read', config.blockedSource), false)
  53. assert.throws(() => fs.readFileSync(config.blockedSource), { code: 'ERR_ACCESS_DENIED' })
  54. report.sourceReadDenied = true
  55. }
  56. await ctx.sessionPersistence.flush()
  57. progress('session attachment')
  58. await registry.attachSession(agent.id)
  59. await ctx.sessionController.rename({ sessionId: agent.id, title: '插件安装验收' })
  60. report.ok = true
  61. } catch (error) {
  62. report.error = error.stack ?? String(error)
  63. throw error
  64. } finally {
  65. fs.writeFileSync(config.report, JSON.stringify(report, null, 2) + '\n')
  66. }
  67. }