@whyshu/argent 0.2.0 → 0.2.1

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.1",
4
4
  "type": "module",
5
5
  "description": "Argent 单 bundle(dsh web / DSH Desktop 同一插件):ERP 连接器 + 问述代理 + 登录/续期 + MCP 网关 + 伞伞桌宠(含积分)",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -23,6 +23,7 @@ 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'
26
27
 
27
28
  const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
28
29
  const PKG_VERSION = (() => {
@@ -133,6 +134,13 @@ export function apply(ctx) {
133
134
  console.error('[argent] 登录端点注册失败:', e?.message || e)
134
135
  }
135
136
 
137
+ // ②.5 机器任务会话登记(计费互斥甲):监听 session/title 命中「后台派发:」⇒ 登记 sessionId
138
+ try {
139
+ installMachineSessionReporter(ctx, { dataDir })
140
+ } catch (e) {
141
+ console.error('[argent] 机器任务会话登记安装失败(不影响其它功能):', e?.message || e)
142
+ }
143
+
136
144
  // ③ 账号面板端点(poller/ui 都注册:积分显示与轮询无关)
137
145
  try {
138
146
  ctx.webServer.register({
@@ -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
+ }