// 一次性校验:把 lib/client.js 装进 mock locale,用上游真实 zh 值核对全部补丁键的输出。 'use strict' const fs = require('fs') const { execFileSync } = require('child_process') // ---------- 上游 zh 词典(摘自 checkout packages/client/**/locales.ts) ---------- const UPSTREAM = { conversation: { 'hint.goal.active': '当前目标进行中。可输入 edit 修改 / pause 暂停 / resume 继续 / clear 清除', // 0.1.7 复验新增:工具详情表字段名(上游 zh 把 Schema 当专有名词保留)。 'detail.field.inputSchema': '输入 Schema', 'detail.field.outputSchema': '输出 Schema', }, // DSH 0.1.2:统计与消息键由 conversation 拆到 chat(ui-chat 包)。 // DSH 0.1.5:chat.stats.* 系列键与 settings.transcript.* 已从上游移除 // (统计行改为 composer-dock StatsPills + TurnUsagePanel;transcript 由 // common 词典覆盖),UPSTREAM 不再收录。 chat: { 'message.compaction.completed': '已压缩 {items} 条历史记录(约 {tokens} tokens)', 'message.unknownSurface': '未知 surface 事件:{type}', 'message.maxTokens': '已达到输出 token 上限', // message.ttft 键已随上游 0.1.2-alpha.2 移除(TTFT 并入 turnTime.ttft)。 'message.tokensPerSecond': '{tps} tok/s', // 上游 0.1.2-alpha.2 新增:回答末尾用量/耗时统计(TurnUsagePanel)。 'message.turnUsage.count': '{count} tok', 'message.turnUsage.consumed': '用量 {total}', 'message.retry.status': '{label}({retry}/{maximum}) · {seconds}s', 'message.turnProcess.subagents.one': '{count} 个 subagent', 'message.turnProcess.subagents.other': '{count} 个 subagent', // 0.1.5 StatsPills 统计对话框:zh 值仍夹带英文 Token/token。 'stats.dialog.usageTitle': 'Token 用量', 'stats.dialog.ttft': '首 token 平均(TTFT)', // 0.1.7 复验新增:速度行的 TPS。 'stats.dialog.speed': '输出速度(TPS)', }, // DSH 0.1.5:trajectory 视图完全词典化(ui-trajectory 包),zh 值仍夹带 // 英文残留(token/tok/tok-s/Schema/Round),由 zh-dict.ts 的 trajectory // partial 修正;UPSTREAM 收录这些键作为 mock 输入。toolbar.* 为旧键 // (上游 0.1.2-alpha.2 已本地化,保留供检查)。 trajectory: { 'unit.tokens': '{value} tok', 'unit.tokensPerSecond': '{value} tok/s', 'usage.tokens': 'Token', 'tab.schema': 'Schema', 'record.schemaUnavailable': 'Schema 不可用', 'timing.firstTokenUnavailable': '首 token 时间不可用', 'timing.outputTokensUnavailable': '输出 token 数不可用', 'timing.ttft': '首 token 延迟', 'timeline.ttftDecoding': '首 token {ttft} · 解码 {decoding}', 'source.goalRound': '目标 · Round {round}', 'toolbar.duration': '时长', 'toolbar.useActualDuration': '使用实际时长', 'toolbar.useEqualWidth': '使用等宽操作', 'toolbar.actualTime': '实际时间', 'toolbar.turns': '轮次', 'toolbar.expandTurns': '展开所有轮次', 'toolbar.collapseTurns': '收起所有轮次', 'toolbar.calls': '调用', 'toolbar.expandCalls': '展开所有调用', 'toolbar.collapseCalls': '收起所有调用', }, 'settings.models': { intro: '填入各提供商的 API 密钥即可使用其模型。', deleteDescriptionWithCredential: '删除 {provider} 会移除其配置和存储的 API 密钥。', credentialConfigured: 'API 密钥已配置', credentialMissing: 'API 密钥缺失', keyInput: 'API 密钥', keyPlaceholder: '输入 API 密钥', keyPlaceholderNative: '输入 API 密钥,或留空使用环境认证', keyBlank: '请输入 API 密钥;留空则保持已存储的密钥。', keyBlankNew: '请输入 API 密钥;若该提供商以其他方式鉴权,可以留空。', keyIllegalCharacters: '该 API 密钥格式错误,请检查。', baseUrl: 'API 地址', modelId: '模型 ID', modelNamePlaceholder: '留空时使用模型 ID', maxTokens: '最大输出 token 数', modelsEmpty: '模型选择器中将不显示任何模型;目录外 ID 仍可直接发送。', modelIdRequired: '模型 ID 不能为空。', modelIdDuplicate: '模型 ID 不能重复。', modelContextInvalid: '上下文窗口必须是正数,例如 131072、256K 或 1M。', modelMaxTokensInvalid: '最大输出 token 数必须是正数,例如 8192、64K 或 1M。', modelCapacityInvalid: '容量需为数字,可加 K 或 M 后缀。', modelDuplicate: '每个模型 ID 只能出现一次。', fetchNeedsBaseUrl: '请先填写 API 地址,再获取。', customRoute: 'Provider ID', customRouteTaken: '已有提供商使用了这个 ID。', customApi: 'API 协议', customNeedsBaseUrl: '自定义模型 API 需要填写 API 地址。', onboardingTitle: '添加一个 API Key 开始使用', keyRequired: '请输入 API 密钥后继续。', }, // DSH 0.1.7:插件配置表单从设置页搬到侧栏插件页,该命名空间只剩下面 5 个键 // (均为干净中文,无需本插件补丁)。旧键 bashDescription / agentLoopTitle / // agentLoopDescription / webSearchApiKey 已实测从部署版消失(源码与构建产物都没有), // 原先的 4 条断言随之删除——插件页卡片标题改由 Config 元数据(数据层)渲染。 'settings.plugins': { nav: '内置插件', title: '内置插件', intro: '查看内置部署的插件列表', tabs: '插件视图', empty: '本部署没有开放任何插件视图。', }, 'settings.agentPreset': { // title 键已随上游移除(改用 nav),不再收录。 seatHint: '选择新任务使用的 Agent 预设', headerHint: '本任务的 Agent 预设,在任务开始时确定', nav: 'Agent 预设', sectionIntro: '选择 Agent 的工具和工作方式。日常任务用「标准模式」,扩展 DSH 的能力用「创造模式」。', presetStandardDescription: '处理代码、文件和资料,适合大多数任务。Agent 会按需使用检索、编辑和终端等工具。', presetPtcName: 'PTC 模式', // 0.1.7 复验:上游把该描述整段重写(新句无 PTC/SDK 字样)→ 本插件的整句覆盖已撤除, // 此处同步为部署版新值,供 EXPECT 侧不再断言。 presetPtcDescription: '包含标准模式的所有能力,更适合批量调用工具,并对结果进行筛选、整理、去重、统计或汇总的任务。', // 0.1.5 minimal 描述:仅提供持久 shell 的单工具编码 Agent(上游 zh 值, // shell 为小写、不在本插件术语表内;Agent 术语命中)。 presetMinimalDescription: 'Agent 仅使用终端工具完成任务,适合测试和对比其基础表现。', presetCordisDescription: '用对话定制 DSH:让 Agent 编写插件,添加新功能或界面;也能组合工具和提示词,创建自己的模式。', // 0.1.7 复验:上游改了值(旧值「用「创造模式」创作自定义预设」),新值夹带 Agent。 creatorDraft: '让 Agent 帮我创建预设模式', }, 'settings.permission': { // 上游 0.1.2-alpha.2 已本地化为完全权限;保留字典供核对同一自本地化后的值。 'confirm.title': '确认启用完全权限?', 'confirm.description': '启用完全权限后,新会话将减少确认步骤,并且可以直接执行更多操作,包括敏感操作、文件修改或外部命令。仅建议在你信任后续任务时使用。', 'confirm.enable': '启用完全权限', }, plan: { 'chip.on.aria': '计划模式已开启,按下关闭', 'chip.on.title': '计划模式已开启 — 点击关闭(/plan off)', }, skill: { 'row.running': '正在加载 skill', 'row.failed': 'skill 加载失败', 'row.stopped': 'skill 加载已中止', }, model: { 'effort.providerDefault': 'Default', }, workspace: { 'status.subagentsRunning.one': '{n} 个子代理运行中', 'status.subagentsRunning.other': '{n} 个子代理运行中', }, // settings.pluginInventory 上游 0.1.2-alpha.2 已重写(会话/全局分组), // 原 cordis 状态键已移除,不再收录。 cordis: { 'panel.trigger': 'Cordis Plugin', 'panel.runningCount': '{count} running', }, 'session-log-download': { 'dialog.preparingTitle': '正在导出 Session', 'dialog.preparingDescription': '正在准备包含当前 Session、子 Session 和附件的 ZIP 文件。', 'dialog.successTitle': 'Session 导出已开始下载', 'dialog.successDescription': '浏览器正在下载 Session ZIP 文件。', 'dialog.errorTitle': 'Session 导出失败', 'dialog.commandFailed': '无法启动 Session 导出。', // 0.1.5 上游新增的菜单项文案仍夹带英文 Session。 'menu.download': '下载 Session 日志', }, // 0.1.7 复验新增的两个命名空间(上游 zh 值夹带 Agent / Shell)。 'settings.pluginInventory': { presetSubtitle: '由 Agent 预设按会话组成', // 用于验证通配表边界:上游整句不得被 ZH['*'].empty 压成「空」。 empty: '暂无插件。', }, sidebarTerminal: { shell: '选择 Shell', shellLoading: '正在读取 Shell…', shellEmpty: '没有可用的 Shell', }, } // 期望输出(与旧版整句覆盖时的显示完全一致,plan 悬停提示按设计保留 /plan 命令) const EXPECT = { conversation: { // goalActions 术语已删(用户接受):该提示保留上游英文命令词 'hint.goal.active': '当前目标进行中。可输入 edit 修改 / pause 暂停 / resume 继续 / clear 清除', // access.confirm.* 上游 0.1.2-alpha.2 已本地化为完全权限,本插件不再覆盖(跟随上游)。 // 0.1.7 新增:Schema → 模式(与 trajectory.tab.schema 同一术语)。 'detail.field.inputSchema': '输入模式', 'detail.field.outputSchema': '输出模式', }, chat: { // chat.stats.* 与 settings.transcript.* 已随上游 0.1.5 移除,无覆盖。 'message.compaction.completed': '已压缩 {items} 条历史记录(约 {tokens} 词元)', 'message.unknownSurface': '未知界面事件:{type}', 'message.maxTokens': '已达到输出词元上限', // message.ttft 键已随上游 0.1.2-alpha.2 移除(TTFT 并入 turnTime.ttft)。 'message.tokensPerSecond': '{tps} 词元/秒', 'message.retry.status': '{label}({retry}/{maximum}) · {seconds}秒', 'message.turnProcess.subagents.one': '{count} 个子代理', 'message.turnProcess.subagents.other': '{count} 个子代理', // 上游 0.1.2-alpha.2 新增回答末尾用量/耗时统计(TurnUsagePanel)译表单键。 'message.turnUsage.count': '{count} 词元', // 0.1.5 StatsPills 统计对话框:Term 层修正成语意中文。 'stats.dialog.usageTitle': '词元用量', 'stats.dialog.ttft': '首词元平均(TTFT)', // 0.1.7 新增:TPS → 词元/秒(与 tokPerSec 术语一致)。 'stats.dialog.speed': '输出速度(词元/秒)', }, // DSH 0.1.5 trajectory partial 期望:残留英文译为中文术语。 trajectory: { 'unit.tokens': '{value} 词元', 'unit.tokensPerSecond': '{value} 词元/秒', 'usage.tokens': '词元', 'tab.schema': '模式', 'record.schemaUnavailable': '模式不可用', 'timing.firstTokenUnavailable': '首词元时间不可用', 'timing.outputTokensUnavailable': '输出词元数不可用', 'timing.ttft': '首词元延迟', 'timeline.ttftDecoding': '首词元 {ttft} · 解码 {decoding}', 'source.goalRound': '目标 · 第 {round} 轮', }, // DSH 0.1.5 起斜杠命令描述由 command 命名空间词典化;本插件保留自定义叫法。 command: { 'description.compact': '压缩较早的对话历史', 'description.export': '将会话日志下载为 ZIP 压缩包', 'description.feedback': '记录对本会话的反馈', 'description.goal': '设置或查看长期任务的目标', 'description.permission': '切换权限预设(沙箱模式 + 审批策略)', }, trajectory: { // 上游 0.1.2-alpha.2 已本地化 trajectory zh 词典,本插件不再覆盖(跟随上游)。 'toolbar.duration': '时长', 'toolbar.useActualDuration': '使用实际时长', 'toolbar.useEqualWidth': '使用等宽操作', 'toolbar.actualTime': '实际时间', 'toolbar.turns': '轮次', 'toolbar.expandTurns': '展开所有轮次', 'toolbar.collapseTurns': '收起所有轮次', 'toolbar.calls': '调用', 'toolbar.expandCalls': '展开所有调用', 'toolbar.collapseCalls': '收起所有调用', }, 'settings.models': { intro: '填入各提供商的接口密钥即可使用其模型。', deleteDescriptionWithCredential: '删除 {provider} 会移除其配置和存储的接口密钥。', credentialConfigured: '接口密钥已配置', credentialMissing: '接口密钥缺失', keyInput: '接口密钥', keyPlaceholder: '输入接口密钥', keyPlaceholderNative: '输入接口密钥,或留空使用环境认证', keyBlank: '请输入接口密钥;留空则保持已存储的密钥。', keyBlankNew: '请输入接口密钥;若该提供商以其他方式鉴权,可以留空。', keyIllegalCharacters: '该接口密钥格式错误,请检查。', baseUrl: '接口地址', modelId: '模型标识', modelNamePlaceholder: '留空时使用模型标识', maxTokens: '最大输出词元数', modelsEmpty: '模型选择器中将不显示任何模型;目录外标识仍可直接发送。', modelIdRequired: '模型标识不能为空。', modelIdDuplicate: '模型标识不能重复。', // 以下三条整句覆盖已删(用户决定):用户需按 K/M 输入,提示保留上游英文单位 modelContextInvalid: '上下文窗口必须是正数,例如 131072、256K 或 1M。', modelMaxTokensInvalid: '最大输出 token 数必须是正数,例如 8192、64K 或 1M。', modelCapacityInvalid: '容量需为数字,可加 K 或 M 后缀。', modelDuplicate: '每个模型标识只能出现一次。', fetchNeedsBaseUrl: '请先填写接口地址,再获取。', customRoute: '提供方标识', customRouteTaken: '已有提供商使用了这个标识。', customApi: '接口协议', customNeedsBaseUrl: '自定义模型接口需要填写接口地址。', onboardingTitle: '添加一个接口密钥开始使用', keyRequired: '请输入接口密钥后继续。', }, // 0.1.7:该命名空间只剩 5 个干净键,无补丁 → 期望值与上游原值一致。 'settings.plugins': { nav: '内置插件', title: '内置插件', intro: '查看内置部署的插件列表', tabs: '插件视图', empty: '本部署没有开放任何插件视图。', }, 'settings.agentPreset': { // title 键已随上游移除(改用 nav),不再覆盖。 seatHint: '选择新任务使用的代理预设', headerHint: '本任务的代理预设,在任务开始时确定', nav: '代理预设', sectionIntro: '选择代理的工具和工作方式。日常任务用「标准模式」,扩展 DSH 的能力用「创造模式」。', presetStandardDescription: '处理代码、文件和资料,适合大多数任务。代理会按需使用检索、编辑和终端等工具。', // presetPtcName:用户自定义叫法(PTC 模式 → 程序模式),本插件整句覆盖,需断言。 // presetPtcDescription:0.1.7 上游整段重写后已撤除覆盖,故此处不断言。 presetPtcName: '程序模式', presetMinimalDescription: '代理仅使用终端工具完成任务,适合测试和对比其基础表现。', presetCordisDescription: '用对话定制 DSH:让代理编写插件,添加新功能或界面;也能组合工具和提示词,创建自己的模式。', creatorDraft: '让代理帮我创建预设模式', }, // settings.permission / permission.access 上游 0.1.2-alpha.2 已本地化为 // 「完全权限」,本插件不再覆盖(跟随上游),因此不从 EXPECT 断言。 plan: { 'chip.on.aria': '计划模式已开启,按下关闭', 'chip.on.title': '计划模式已开启 — 点击关闭(/plan off)', }, skill: { 'row.running': '正在加载技能', 'row.failed': '技能加载失败', 'row.stopped': '技能加载已中止', }, model: { 'effort.providerDefault': '默认', retry: '重试', }, question: { submit: '提交', // 上游 common 词典为「正在提交…」;zh_pro 已不再覆盖此词(0.1.5 跟随上游)。 submitting: '正在提交…', }, workspace: { 'status.subagentsRunning.one': '{n} 个子代理运行中', 'status.subagentsRunning.other': '{n} 个子代理运行中', }, // settings.pluginInventory 上游 0.1.2-alpha.2 已整体重写(会话/全局分组), // 原 cordis 状态键已移除,本插件不再覆盖,因此不从 EXPECT 断言。 cordis: { 'panel.trigger': 'Cordis 插件', 'panel.runningCount': '{count} 个运行中', }, 'session-log-download': { 'dialog.preparingTitle': '正在导出会话', 'dialog.preparingDescription': '正在准备包含当前会话、子会话和附件的 ZIP 文件。', 'dialog.successTitle': '会话导出已开始下载', 'dialog.successDescription': '浏览器正在下载会话 ZIP 文件。', 'dialog.errorTitle': '会话导出失败', 'dialog.commandFailed': '无法启动会话导出。', }, // 0.1.7 新增命名空间期望:Agent → 代理、Shell → 终端。 'settings.pluginInventory': { presetSubtitle: '由代理预设按会话组成', }, sidebarTerminal: { shell: '选择终端', shellLoading: '正在读取终端…', shellEmpty: '没有可用的终端', }, } // ---------- 装载 client.js ---------- let captured = null globalThis.window = { __ModuleLoader__: { load: function (entry) { captured = entry } }, } const src = fs.readFileSync(__dirname + '/lib/client.js', 'utf8') eval(src) if (captured === null || captured.id !== 'deepseek-harness-zh_pro') { console.error('FAIL: client.js 未通过 __ModuleLoader__.load 注册') process.exit(1) } // 搜索凭据快照覆盖(见 mock React 的 useSyncExternalStore);null = 用组件真实默认值。 let credSnapshotOverride: any = null // 插件同步快照覆盖(同上:该 store 的更新走异步 fetch,同步脚本里无法自然驱动)。 // 区分依据是搜索凭据快照带 configured 字段、插件同步快照带 entries 字段。 let syncSnapshotOverride: any = null // 「智能搜索」按钮的 POST 记录:点击切换时组件会调用主机路由,这里同步记账 // (mock fetch 立即 resolve,不引入异步等待)。 const searchToggleCalls: Array<{ session?: string; enabled?: boolean }> = [] globalThis.fetch = function (url: string, init?: any) { if (typeof url === 'string' && url.indexOf('/dsh-zh/api/search-toggle') !== -1) { let body: any = {} try { body = JSON.parse(init?.body ?? '{}') } catch { body = {} } searchToggleCalls.push({ session: body.session, enabled: body.enabled }) } return Promise.resolve({ ok: true, status: 200, json: function () { return Promise.resolve({ ok: true, value: {} }) } }) as any } // 设置页 hook 状态:按组件函数对象分别保存 useState 快照(见 mock React)。 const hookStacks = new WeakMap() let hookOwner: object | null = null let hookIndex = 0 const pluginExports = captured.factory(function (name) { // 本 bundle 唯一允许的跨包引用是 react(设置页组件用);其余跨包 require 视为回归。 if (name === 'react') { return { useSyncExternalStore: function (_subscribe, getSnapshot) { const snapshot = getSnapshot() // 搜索凭据 store 的快照带 configured 字段:用覆盖值模拟主机回的凭据状态 //(该 store 的更新走异步 fetch,同步脚本里无法自然驱动)。 if (credSnapshotOverride !== null && snapshot !== null && typeof snapshot === 'object' && 'configured' in snapshot) { return credSnapshotOverride } if (syncSnapshotOverride !== null && snapshot !== null && typeof snapshot === 'object' && 'entries' in snapshot) { return syncSnapshotOverride } return snapshot }, // 设置页的草稿层(pending / saveStatus / 输入框本地态)依赖 useState 真正 // 保存值:没有 state 就测不了「改动先暂存、点保存才写入」的新契约。 // 按组件函数对象各存一份 hook 数组,重新渲染时保留 state(模拟真实 hook)。 useState: function (initial: unknown) { const owner = hookOwner if (owner === null) return [initial, function () {}] let list = hookStacks.get(owner) if (list === undefined) { list = [] hookStacks.set(owner, list) } const index = hookIndex hookIndex += 1 if (!(index in list)) list[index] = initial const set = function (next: unknown) { list[index] = typeof next === 'function' ? (next as (prev: unknown) => unknown)(list[index]) : next } return [list[index], set] }, // 设置页「网络搜索」卡在挂载时用 useEffect 拉取凭据状态;mock 不跑副作用, // 只保证调用不抛(真实行为由 verify-cli 的主机侧用例覆盖)。 useEffect: function () {}, useRef: function (initial) { return { current: initial } }, createElement: function (type, props) { const children = Array.prototype.slice.call(arguments, 2) const nextProps = Object.assign({}, props, { children: children }) if (typeof type === 'function') { const prevOwner = hookOwner const prevIndex = hookIndex hookOwner = type as object hookIndex = 0 try { return (type as (p: unknown) => unknown)(nextProps) } finally { hookOwner = prevOwner hookIndex = prevIndex } } return { type: type, props: nextProps } }, } } throw new Error('不应发生跨包 require: ' + name) }) // ---------- mock locale / ctx ---------- const COMMON = { submit: '提交', submitting: '正在提交…', retry: '重试' } let active = 'zh' let localeRegisterDisposed = 0 let settingsRender = null const registeredDicts = {} const localeListeners = [] const locale = { getLocale: function () { return { active: active } }, lookup: function (ns, key) { const own = registeredDicts[ns] && registeredDicts[ns][active] if (own && own[key] !== undefined) return own[key] const d = UPSTREAM[ns] if (d && d[key] !== undefined) return d[key] if (COMMON[key] !== undefined) return COMMON[key] return key }, translate: function (ns, key, params) { let out = locale.lookup(ns, key) if (params && typeof params === 'object') { for (const k of Object.keys(params)) { out = out.split('{' + k + '}').join(String(params[k])) } } return out }, register: function (ns, dicts) { registeredDicts[ns] = dicts return function () { if (registeredDicts[ns] === dicts) delete registeredDicts[ns] localeRegisterDisposed += 1 } }, bind: function (ns) { return function (key, params) { return locale.translate(ns, key, params) } }, subscribe: function (listener) { localeListeners.push(listener) return function () { const i = localeListeners.indexOf(listener) if (i !== -1) localeListeners.splice(i, 1) } }, } const ctx = { locale: locale, // 服务监控 tab 注册用的服务注入记录(sidebarRightTabs/slots 由 ctx.inject 等待; // mock 立即回调并把注销函数压入 _effects,与真实生命周期同形)。 _injectCalls: [], inject: function (names, callback) { const record = { names: names, callback: callback, disposed: false } ctx._injectCalls.push(record) // 回调返回的清理函数由 seat.dispose 级联(与真实 Cordis 生命周期同形: // 服务消失/seat 释放时先运行注入体内的清理,再标记记录)。 const cleanup = callback(ctx) const seat = { dispose: function () { record.disposed = true if (typeof cleanup === 'function') cleanup() return undefined }, } return seat }, slots: { // 与真实契约一致:inject 返回 dispose(本项目既有代码 // `disposers.push(slots.inject(...))` 就依赖这一点)。返回 undefined 会让 // 依赖它的挂载/卸载(如「智能搜索」开关的重挂载)在桩下静默失效。 inject: function (_name, setup) { const dispose = setup() if (typeof dispose === 'function') ctx._effects.push(dispose) return function () { if (typeof dispose === 'function') dispose() } }, // 槽位条目的渲染函数按槽位名分表保存:本插件同时注册设置分区 // (settings.section)与输入框胶囊(conversation.input.left), // 单变量会让后注册的覆盖前者的 settingsRender。 register: function (config, render) { const name = config !== null && config !== undefined ? config.name : undefined if (name === 'conversation.input.left') { ctx._composerEntries.push({ id: config.id, order: config.order, render: render, inject: config.inject, label: config.label }) return function () { const index = ctx._composerEntries.findIndex(function (entry) { return entry.render === render }) if (index !== -1) ctx._composerEntries.splice(index, 1) } } settingsRender = render return function () { settingsRender = null } }, }, _effects: [], // 输入框下方工具行的槽位条目(conversation.input.left):智能搜索胶囊在此登记。 _composerEntries: [], // 可选服务表:服务监控 tab 注册测试注入 sidebarRightTabs mock;真实代码用 // ctx.get 读取,mock 在此表命中时优先返回。 _services: {}, get: function (name) { if (name === 'locale') return locale if (Object.prototype.hasOwnProperty.call(ctx._services, name)) return ctx._services[name] // 声明在 ctx 上的服务(slots 等)与 _services 同源可取:bundle 代码用 // ctx.get('slots') 读服务,mock 直接回退到自身属性。 if (Object.prototype.hasOwnProperty.call(ctx, name)) return ctx[name] return undefined }, on: function () { return function () {} }, off: function () {}, effect: function (fn) { const dispose = fn() if (typeof dispose === 'function') this._effects.push(dispose) }, } // ---------- 权限标签 / 设置开关 / 生命周期 DOM:用最小假 DOM 验证 ---------- const fakeObserverCbs = [] globalThis.MutationObserver = class { constructor(cb) { this.cb = cb; fakeObserverCbs.push(this) } observe() {} disconnect() {} } function makeStyle() { const values = {} return { fontSize: '', setProperty: function (name, value) { values[name] = String(value) }, removeProperty: function (name) { delete values[name] if (name === 'font-size') this.fontSize = '' }, getPropertyValue: function (name) { return values[name] || '' }, } } function makeText(data) { return { nodeType: 3, data: data, parentElement: null, nextSibling: null } } // 权限预设标签(Workspace Write 等)自 0.1.2-alpha.2 起由上游本地化, // 本插件只保留 host 下发的权限描述改写(见 PERMISSION_DESCRIPTIONS)。 const permissionDescEn = 'Write inside the workspace and permitted temporary directories; wider retries require approval.' const permissionDescZh = '仅可写入工作区与允许的临时目录;更宽的权限需单独批准。' const permissionText = makeText(permissionDescEn) const commandText = makeText('Compact older conversation history') const thinkText = makeText('Think') const toolText = makeText('Tool call') const deepThinkText = makeText('Deep diving...') // 斜杠菜单技能描述(shipped SKILL.md frontmatter 原文,与 dom-labels.ts 逐字一致) const skillCompDescEn = 'Use when creating, changing, or validating a Cordis composition for this harness — writing or editing an agent preset, adding or removing a plugin row, deciding whether something belongs to the host composition or to one session, checking whether a preset you authored actually mounts, or diagnosing a row that mounted but contributed nothing.' const skillDevDescEn = 'Create, modify, debug, or extend dynamic Cordis Plugins, including Host Services and Events, Client Slot and theme UI, Package-private Client-to-Host calls, dynamic Tools, version updates, approval failures, and runtime diagnostics. Use this Skill to route a user request to the correct platform and Inspect Provider, then define, run, repair, or roll back the Plugin.' const skillCompText = makeText(skillCompDescEn) const skillDevText = makeText(skillDevDescEn) permissionText.nextSibling = commandText commandText.nextSibling = thinkText thinkText.nextSibling = toolText toolText.nextSibling = deepThinkText deepThinkText.nextSibling = skillCompText skillCompText.nextSibling = skillDevText // 思考块 DOM 夹具:最小化的元素对象,支撑「默认展开行数」折叠逻辑的查询/读写。 let injectedThinkRoots = [] function makeFakeEl(attrs) { const state = {} const el = { nodeType: 1, tagName: 'DIV', textContent: '', parentElement: null, parentNode: null, childNodes: [], firstChild: null, firstElementChild: null, nextSibling: null, previousSibling: null, style: {}, scrollTop: 0, getAttribute: function (name) { return Object.prototype.hasOwnProperty.call(state, name) ? state[name] : null }, setAttribute: function (name, value) { state[name] = String(value) }, removeAttribute: function (name) { delete state[name] }, hasAttribute: function (name) { return Object.prototype.hasOwnProperty.call(state, name) }, appendChild: function (node) { node.parentNode = node.parentElement = el const last = this.childNodes.length > 0 ? this.childNodes[this.childNodes.length - 1] : null node.previousSibling = last node.nextSibling = null if (last !== null && last !== undefined) last.nextSibling = node this.childNodes.push(node) if (this.firstChild === null) this.firstChild = node if (this.firstElementChild === null && node.nodeType === 1) this.firstElementChild = node if (node.onPush) node.onPush() return node }, insertBefore: function (node, refNode) { if (refNode === null || refNode === undefined) return this.appendChild(node) node.parentNode = node.parentElement = el const i = this.childNodes.indexOf(refNode) if (i === -1) return this.appendChild(node) node.previousSibling = i > 0 ? this.childNodes[i - 1] : null node.nextSibling = refNode if (node.previousSibling !== null) node.previousSibling.nextSibling = node refNode.previousSibling = node this.childNodes.splice(i, 0, node) if (this.firstChild === refNode) this.firstChild = node if (this.firstElementChild === refNode && node.nodeType === 1) this.firstElementChild = node if (node.onPush) node.onPush() return node }, removeChild: function (node) { const i = this.childNodes.indexOf(node) if (i !== -1) { const prev = i > 0 ? this.childNodes[i - 1] : null const next = i + 1 < this.childNodes.length ? this.childNodes[i + 1] : null if (prev !== null) prev.nextSibling = next if (next !== null) next.previousSibling = prev this.childNodes.splice(i, 1) if (this.firstChild === node) this.firstChild = next if (this.firstElementChild === node) this.firstElementChild = next } node.parentNode = node.parentElement = null node.previousSibling = null node.nextSibling = null return node }, querySelector: function () { return null }, querySelectorAll: function () { return [] }, addEventListener: function (type, fn) { el._handlers[type] = fn }, removeEventListener: function (type) { delete el._handlers[type] }, _handlers: {}, } // scrollHeight/clientHeight 动态派生:行高 24px(与 DSH thinkBody CSS 一致), // clientHeight 受 style.maxHeight 截断。 Object.defineProperty(el, 'scrollHeight', { get: function () { return Math.max(1, String(el.textContent).split('\n').length * 24) }, }) Object.defineProperty(el, 'clientHeight', { get: function () { const max = parseFloat(el.style.maxHeight) const full = Math.max(1, String(el.textContent).split('\n').length * 24) return Number.isFinite(max) && max > 0 ? Math.min(full, max) : full }, }) el.click = function (type) { const fn = el._handlers[type] if (typeof fn === 'function') fn() } return el } // CSS 折叠语义下的可见行数读取(正文文本始终为全文)。 function shownOf(body) { return body !== null && body.__dshZhThink !== undefined ? body.__dshZhThink.shown : -1 } const fakeBody = { nodeType: 1, tagName: 'BODY', style: makeStyle(), getAttribute: function () { return null }, setAttribute: function () {}, firstChild: permissionText, nextSibling: null, querySelector: function () { return null }, querySelectorAll: function (selector) { if (selector === '[data-variant="think"]') return injectedThinkRoots if (selector === '[data-dsh-zh-hide-prompt-provider]') return [] return [] }, } for (const node of [permissionText, commandText, thinkText, toolText, deepThinkText, skillCompText, skillDevText]) node.parentElement = fakeBody window.innerWidth = 1280 window.getComputedStyle = function () { return { textOverflow: 'clip', lineHeight: '24px', fontSize: '14px' } } window.addEventListener = function () {} window.removeEventListener = function () {} // 刻意**不**提供 document.head:本桩只覆盖「词典 + DOM 文本层」这条面, // 样式注入子系统(archive-view / session-menu / session-batch / auto-archive / // service-monitor / search-toggle 六处都会往 head append