@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 +4 -1
- package/src/index.js +16 -0
- package/src/machine-session.mjs +114 -0
- package/src/office-runtime.js +23 -1
- package/src/runtime-sync.mjs +113 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whyshu/argent",
|
|
3
|
-
"version": "0.2.
|
|
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
|
+
}
|
package/src/office-runtime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|