@whyshu/argent 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whyshu/argent",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Argent 单 bundle(dsh web / DSH Desktop 同一插件):ERP 连接器 + 问述代理 + 登录/续期 + MCP 网关 + 伞伞桌宠(含积分)",
6
6
  "main": "src/index.js",
@@ -36,5 +36,8 @@
36
36
  "plugin": {
37
37
  "displayName": "Argent(ERP + 问述代理 + 伞伞)"
38
38
  }
39
+ },
40
+ "dependencies": {
41
+ "@whyshu/argent-engine": "0.8.0-rc.12"
39
42
  }
40
43
  }
package/src/index.js CHANGED
@@ -23,6 +23,8 @@ import { startOfficeRuntime } from './office-runtime.js'
23
23
  import { registerWhyshuAuth } from './whyshu-auth.mjs'
24
24
  import { acquireLease, heartbeatLease, releaseLease, leaseHolder } from './lease.mjs'
25
25
  import { resolveAccountToken } from './account-token.mjs'
26
+ import { installMachineSessionReporter } from './machine-session.mjs'
27
+ import { syncRuntimeIfNeeded } from './runtime-sync.mjs'
26
28
 
27
29
  const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
28
30
  const PKG_VERSION = (() => {
@@ -133,6 +135,13 @@ export function apply(ctx) {
133
135
  console.error('[argent] 登录端点注册失败:', e?.message || e)
134
136
  }
135
137
 
138
+ // ②.5 机器任务会话登记(计费互斥甲):监听 session/title 命中「后台派发:」⇒ 登记 sessionId
139
+ try {
140
+ installMachineSessionReporter(ctx, { dataDir })
141
+ } catch (e) {
142
+ console.error('[argent] 机器任务会话登记安装失败(不影响其它功能):', e?.message || e)
143
+ }
144
+
136
145
  // ③ 账号面板端点(poller/ui 都注册:积分显示与轮询无关)
137
146
  try {
138
147
  ctx.webServer.register({
@@ -170,6 +179,13 @@ export function apply(ctx) {
170
179
 
171
180
  // ⑤ 内核解析 → 租约 → MCP 网关 → ERP 连接器(全部在异步块里,串行避免 dataDir 竞态)
172
181
  void (async () => {
182
+ // 运行时跟随插件:插件 pin 的 engine 版本若新于 ~/.argent-dsh,先同步(失败只告警)
183
+ try {
184
+ const r = syncRuntimeIfNeeded({ log: (m) => console.log(m) })
185
+ if (r && r.action !== 'up_to_date' && r.action !== 'installed_newer') console.log('[argent] runtime sync:', JSON.stringify(r))
186
+ } catch (e) {
187
+ console.error('[argent] 运行时同步失败(继续启动):', e?.message || e)
188
+ }
173
189
  try {
174
190
  const k = await resolveKernel().catch(() => null)
175
191
  if (k?.mod?.resolveDataDir) {
@@ -0,0 +1,114 @@
1
+ /**
2
+ * 机器任务会话登记(2026-11-07,计费互斥甲端到端;与 whyshu-svr machine-sessions 配套)
3
+ *
4
+ * 链路:argent-runner → dsh /api/task-board → agent → 问述代理。
5
+ * runner 建卡时把标题设为 `后台派发:<preset>`;dsh 会把标题写入 `session/title` 事件。
6
+ * 本模块监听会话标题事件,命中机器任务前缀时把 sessionId 登记到服务端(TTL),
7
+ * 代理侧随后按 `x-deepseek-harness-session-id` 命中注册表 ⇒ 只记 cost_audit(0 积分)。
8
+ *
9
+ * 失败策略:任何异常只告警,不影响任务执行/聊天。
10
+ */
11
+ import { existsSync, readFileSync } from 'node:fs'
12
+ import { homedir } from 'node:os'
13
+ import { join } from 'node:path'
14
+
15
+ export const DEFAULT_TITLE_PREFIX = '后台派发:'
16
+
17
+ /** 纯函数:标题是否机器任务(runner 建卡前缀;可被 env 覆盖)。 */
18
+ export function isMachineTaskTitle(title, prefix = DEFAULT_TITLE_PREFIX) {
19
+ const t = String(title || '').trim()
20
+ return Boolean(t) && t.startsWith(prefix)
21
+ }
22
+
23
+ /** 读取本机 agent-report(machineId / machineToken);读不到返回空。 */
24
+ export function readAgentReport(dataDir) {
25
+ try {
26
+ const dir = dataDir || process.env.ARGENT_DATA_DIR || join(homedir(), '.dsh', 'argent')
27
+ const file = join(dir, 'agent-report.json')
28
+ if (!existsSync(file)) return {}
29
+ const raw = JSON.parse(readFileSync(file, 'utf8'))
30
+ return {
31
+ machineId: String(raw?.machineId || '').trim(),
32
+ machineToken: String(raw?.machineToken || '').trim(),
33
+ tenantAppId: String(raw?.tenantAppId || '').trim(),
34
+ serverUrl: String(raw?.serverUrl || '').trim(),
35
+ }
36
+ } catch {
37
+ return {}
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 安装会话标题监听:命中机器任务 ⇒ 登记 sessionId(幂等;同会话只登记一次)。
43
+ * @returns {{stop:()=>void, registered:Set<string>, debug:()=>object}}
44
+ */
45
+ export function installMachineSessionReporter(ctx, {
46
+ dataDir = process.env.ARGENT_DATA_DIR || join(homedir(), '.dsh', 'argent'),
47
+ tenant = process.env.ARGENT_TENANT || 'ly-0001',
48
+ apiBase = (process.env.ARGENT_API_BASE || 'https://whyshu.com').replace(/\/+$/, ''),
49
+ prefix = process.env.ARGENT_MACHINE_TITLE_PREFIX || DEFAULT_TITLE_PREFIX,
50
+ fetchImpl = globalThis.fetch,
51
+ log = (m) => console.log(`[argent] ${m}`),
52
+ } = {}) {
53
+ const registered = new Set()
54
+ const disposers = []
55
+ const report = readAgentReport(dataDir)
56
+
57
+ const debug = () => ({
58
+ prefix,
59
+ hasMachineToken: Boolean(report.machineToken),
60
+ machineId: report.machineId,
61
+ registered: [...registered],
62
+ })
63
+
64
+ const register = async (sessionId) => {
65
+ const sid = String(sessionId || '').trim()
66
+ if (!sid || registered.has(sid)) return
67
+ registered.add(sid)
68
+ if (!report.machineToken) {
69
+ log(`机器任务会话 ${sid} 未登记:缺 agent-report.machineToken`)
70
+ return
71
+ }
72
+ try {
73
+ const res = await fetchImpl(`${apiBase}/api/argent/v1/tenants/${encodeURIComponent(tenant)}/machine-sessions`, {
74
+ method: 'POST',
75
+ headers: { 'content-type': 'application/json', 'x-api-key': report.machineToken },
76
+ body: JSON.stringify({ sessionId: sid, machineId: report.machineId }),
77
+ signal: AbortSignal.timeout(5000),
78
+ })
79
+ if (!res.ok) {
80
+ registered.delete(sid)
81
+ log(`机器任务会话登记失败(HTTP ${res.status}):${sid}`)
82
+ } else {
83
+ log(`机器任务会话已登记:${sid}`)
84
+ }
85
+ } catch (e) {
86
+ registered.delete(sid)
87
+ log(`机器任务会话登记异常:${String(e?.message || e).slice(0, 120)}`)
88
+ }
89
+ }
90
+
91
+ const checkEvent = (session, event) => {
92
+ try {
93
+ if (!event || event.type !== 'session/title') return
94
+ const data = event.data || {}
95
+ if (!isMachineTaskTitle(data.title, prefix)) return
96
+ void register(session?.id || event.sessionId)
97
+ } catch { /* best-effort */ }
98
+ }
99
+
100
+ try { disposers.push(ctx.on('session/event', checkEvent)) } catch { /* 宿主无此事件 */ }
101
+ try {
102
+ disposers.push(ctx.on('session/created', (session) => {
103
+ // 少数宿主在 created 时已带标题(兜底;通常标题事件稍后到)
104
+ const title = session?.header?.title || session?.title
105
+ if (isMachineTaskTitle(title, prefix)) void register(session?.id)
106
+ }))
107
+ } catch { /* 宿主无此事件 */ }
108
+
109
+ return {
110
+ stop: () => { for (const d of disposers) { try { d?.() } catch { /* noop */ } } },
111
+ registered,
112
+ debug,
113
+ }
114
+ }
@@ -414,6 +414,17 @@ export function startOfficeRuntime(ctx, { dataDir } = {}) {
414
414
  const CHAT_HEARTBEAT_MS = 30 * 1000
415
415
  const TASK_MAX_MS = 45 * 60 * 1000
416
416
 
417
+ // ── 办公室角色(P2 机器对话/新建会话)────────────────────────────────────
418
+ // 与 `plugins/argent-erp/presets/argent-office.patch.yml` 对齐:
419
+ // · ⚠️ **不**把 create_session 的缺省 preset 改成 argent-office —— ERP「AI 团队」是问述内部用的;
420
+ // · 只有 ERP **显式**选到 argent-office 时,才把该会话模型钉死为问述代理(走问述积分)。
421
+ // 均可由 env 覆盖,交给客户机按部署改,不必改代码。
422
+ const OFFICE_PRESET = process.env.ARGENT_OFFICE_PRESET || 'argent-office'
423
+ const OFFICE_MODEL = {
424
+ provider: process.env.ARGENT_OFFICE_MODEL_PROVIDER || 'whyshu',
425
+ model: process.env.ARGENT_OFFICE_MODEL || 'deepseek-flash',
426
+ }
427
+
417
428
  ctx.effect(() => {
418
429
  const off = ctx.on('session/event', (session, event) => {
419
430
  const sid = String(session?.id || '')
@@ -864,9 +875,20 @@ export function startOfficeRuntime(ctx, { dataDir } = {}) {
864
875
  try {
865
876
  const sc = ctx.get('sessionController') || ctx.sessionController
866
877
  if (!sc || typeof sc.create !== 'function') throw new Error('sessionController.create 不可用(该 dsh 版本不支持)')
867
- const created = await sc.create({ agentPreset: String(command.preset || '') || undefined })
878
+ // ⚠️ ERP「AI 团队」是问述内部用的,不要把它缺省接管成六禾园办公室角色;
879
+ // 缺省仍是宿主默认(standard),只有 ERP 显式指定 preset 时才按指定来。
880
+ const preset = String(command.preset || '') || undefined
881
+ const created = await sc.create({ agentPreset: preset })
868
882
  resultSessionId = String(created?.sessionId || '')
869
883
  if (!resultSessionId) throw new Error('create 未返回 sessionId')
884
+ // 办公室角色:把模型钉成问述代理(失败不致命,会话照常可用)
885
+ if (preset === OFFICE_PRESET && typeof sc.selectModel === 'function') {
886
+ try {
887
+ await sc.selectModel({ sessionId: resultSessionId, ...OFFICE_MODEL })
888
+ } catch (e) {
889
+ log('warn', `会话模型钉选失败(不影响新建):${trunc(e?.message || e, 160)}`)
890
+ }
891
+ }
870
892
  } catch (e) {
871
893
  status = 'failed'
872
894
  error = trunc(e?.message || e, 300)
@@ -0,0 +1,113 @@
1
+ /**
2
+ * 运行时跟随插件(2026-11-07):插件启动时把依赖的 @whyshu/argent-engine 同步到 ~/.argent-dsh。
3
+ *
4
+ * 目标:以后 UI 升级 @whyshu/argent 插件,pnpm 会同时装上插件 pin 的 engine 依赖;
5
+ * 插件启动时把 engine 文件刷到运行时目录 ⇒ 不再需要启动器/单独 runtime install。
6
+ *
7
+ * 安全边界:
8
+ * · 只写引擎文件(scripts/core/verticals/configs/agents/skills/tools/agentic + VERSION);
9
+ * ⛔ 不动 venv、数据、台账、agent-report、license;
10
+ * · 只在 bundled 版本比已装新(或未装)时同步;已装更新则跳过(防降级);
11
+ * · 覆盖前备份差异文件到 <target>/.runtime-backup/<ts>/;失败只告警不阻断插件启动。
12
+ */
13
+ import { createRequire } from 'node:module'
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, statSync, chmodSync, readdirSync } from 'node:fs'
15
+ import { dirname, join, relative, sep } from 'node:path'
16
+ import { homedir } from 'node:os'
17
+ import { fileURLToPath } from 'node:url'
18
+ import { createHash } from 'node:crypto'
19
+
20
+ const sha = (buf) => createHash('sha256').update(buf).digest('hex')
21
+
22
+ /** 版本比较(支持 0.8.0-rc.11 / 0.8.0 / 1.2.3);无法解析时按字符串比较。 */
23
+ export function compareVersions(a, b) {
24
+ const parse = (v) => {
25
+ const m = String(v || '').trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([a-z]+)\.?(\d+)?)?/i)
26
+ if (!m) return null
27
+ return [Number(m[1]), Number(m[2]), Number(m[3]), m[4] || '', Number(m[5] || 0)]
28
+ }
29
+ const x = parse(a); const y = parse(b)
30
+ if (!x || !y) return String(a || '').localeCompare(String(b || ''))
31
+ for (let i = 0; i < 3; i += 1) if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1
32
+ // 正式版 > 预发布(0.8.0 > 0.8.0-rc.12)
33
+ if (!x[3] && y[3]) return 1
34
+ if (x[3] && !y[3]) return -1
35
+ if (x[3] !== y[3]) return String(x[3]).localeCompare(String(y[3]))
36
+ if (x[4] !== y[4]) return x[4] < y[4] ? -1 : 1
37
+ return 0
38
+ }
39
+
40
+ /** 解析插件依赖的 engine 包目录(未安装返回 '')。 */
41
+ export function resolveBundledEngineDir() {
42
+ try {
43
+ const require = createRequire(import.meta.url)
44
+ const pkgJson = require.resolve('@whyshu/argent-engine/package.json')
45
+ return dirname(pkgJson)
46
+ } catch {
47
+ return ''
48
+ }
49
+ }
50
+
51
+ export function installedRuntimeVersion(target = join(homedir(), '.argent-dsh')) {
52
+ try { return String(readFileSync(join(target, 'VERSION'), 'utf8')).trim() } catch { return '' }
53
+ }
54
+
55
+ function listFiles(dir, base = dir) {
56
+ const out = []
57
+ for (const e of readdirSync(dir)) {
58
+ const p = join(dir, e)
59
+ if (statSync(p).isDirectory()) out.push(...listFiles(p, base))
60
+ else out.push(relative(base, p).split(sep).join('/'))
61
+ }
62
+ return out
63
+ }
64
+
65
+ /**
66
+ * 同步运行时(bundle → target)。
67
+ * @returns {{ok:boolean, action:'no_bundle'|'up_to_date'|'installed_newer'|'synced', from?:string, to?:string, files?:number, backupDir?:string}}
68
+ */
69
+ export function syncRuntimeIfNeeded({
70
+ target = join(homedir(), '.argent-dsh'),
71
+ bundledDir = resolveBundledEngineDir(),
72
+ log = () => {},
73
+ } = {}) {
74
+ if (!bundledDir || !existsSync(join(bundledDir, 'engine'))) return { ok: false, action: 'no_bundle' }
75
+ const src = join(bundledDir, 'engine')
76
+ const bundledVersion = (() => { try { return readFileSync(join(src, 'VERSION'), 'utf8').trim() } catch { return '' } })()
77
+ if (!bundledVersion) return { ok: false, action: 'no_bundle' }
78
+ const current = installedRuntimeVersion(target)
79
+ if (current && compareVersions(bundledVersion, current) < 0) {
80
+ return { ok: true, action: 'installed_newer', from: current, to: bundledVersion }
81
+ }
82
+ if (current && compareVersions(bundledVersion, current) === 0) {
83
+ return { ok: true, action: 'up_to_date', from: current, to: bundledVersion }
84
+ }
85
+ const manifestPath = join(bundledDir, 'MANIFEST.json')
86
+ let manifest = null
87
+ try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) } catch { manifest = null }
88
+ const files = listFiles(src)
89
+ const ts = new Date().toISOString().replace(/[:.]/g, '-')
90
+ const backup = join(target, '.runtime-backup', `plugin-sync-${ts}`)
91
+ mkdirSync(backup, { recursive: true })
92
+ let copied = 0
93
+ for (const rel of files) {
94
+ const s = join(src, rel)
95
+ const d = join(target, rel)
96
+ if (existsSync(d)) {
97
+ try {
98
+ if (sha(readFileSync(d)) !== sha(readFileSync(s))) {
99
+ const b = join(backup, rel)
100
+ mkdirSync(dirname(b), { recursive: true })
101
+ copyFileSync(d, b)
102
+ }
103
+ } catch { /* 备份失败继续 */ }
104
+ }
105
+ mkdirSync(dirname(d), { recursive: true })
106
+ copyFileSync(s, d)
107
+ chmodSync(d, rel.endsWith('.sh') ? 0o755 : 0o644)
108
+ copied += 1
109
+ }
110
+ writeFileSync(join(target, 'VERSION'), `${bundledVersion}\n`)
111
+ log(`[argent] 运行时已跟随插件更新:${current || '(未安装)'} → ${bundledVersion}(${copied} 个文件,备份 ${backup})`)
112
+ return { ok: true, action: 'synced', from: current || '', to: bundledVersion, files: copied, backupDir: backup, manifest: Boolean(manifest) }
113
+ }