dsh-agentone 0.5.17 → 0.5.18
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/README.md +3 -0
- package/lib/index.js +48 -0
- package/lib/store.js +43 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -61,6 +61,9 @@ dsh plugin --profile web add dsh-agentone
|
|
|
61
61
|
- `$DSH_HOME/.credentials.yaml` 写入 `refs.AGENTONE_API_KEY`(0600),内容
|
|
62
62
|
是 30 天插件令牌而非真实模型密钥——密钥不出平台,TokenHub 按用户计量;
|
|
63
63
|
- 只覆盖 `platform` 命名空间,用户自己配置的其它 provider 原样保留;
|
|
64
|
+
- 同步失败(如平台侧 AirRouter 瞬时不可达)不阻断登录/启动,原因记录在
|
|
65
|
+
状态页「同步状态」;状态页轮询发现残留错误时按指数退避(1min 起、
|
|
66
|
+
封顶 30min、最多 30 次)自动重试,平台恢复后无需重启 dsh 即自愈;
|
|
64
67
|
- 「退出登录」(页头按钮)会吊销平台令牌、移除凭据引用与 platform
|
|
65
68
|
provider(其它 provider 的默认模型设置不受影响);凭据文件损坏时也能
|
|
66
69
|
正常退出(坏文件移开为 `credentials.json.corrupt`)。
|
package/lib/index.js
CHANGED
|
@@ -194,6 +194,41 @@ function pluginProfileDir(config) {
|
|
|
194
194
|
return join(resolveDshHome(config), 'profiles', PLUGIN_PROFILE);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/** 模型同步失败自愈重试调度(模块级单例:apply 重入先清旧定时器)。
|
|
198
|
+
* 平台侧瞬时故障(如 AirRouter 短暂不可达)会让启动同步失败,错误一直
|
|
199
|
+
* 挂在状态页直到下次 dsh 重启——这里由 5s 状态轮询驱动:发现 model_error
|
|
200
|
+
* 就按指数退避后台重试,成功即清错误,无需用户干预。 */
|
|
201
|
+
export const MODEL_RETRY_BASE_MS = 60_000;
|
|
202
|
+
export const MODEL_RETRY_MAX_MS = 30 * 60_000;
|
|
203
|
+
export const MODEL_RETRY_MAX_ATTEMPTS = 30;
|
|
204
|
+
let modelRetryTimer = null;
|
|
205
|
+
let modelRetryAttempts = 0;
|
|
206
|
+
|
|
207
|
+
function clearModelRetry() {
|
|
208
|
+
if (modelRetryTimer !== null) {
|
|
209
|
+
clearTimeout(modelRetryTimer);
|
|
210
|
+
modelRetryTimer = null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function scheduleModelRetry(config, log) {
|
|
215
|
+
if (modelRetryTimer !== null || modelRetryAttempts >= MODEL_RETRY_MAX_ATTEMPTS) return;
|
|
216
|
+
const delay = Math.min(MODEL_RETRY_BASE_MS * 2 ** modelRetryAttempts, MODEL_RETRY_MAX_MS);
|
|
217
|
+
modelRetryAttempts += 1;
|
|
218
|
+
modelRetryTimer = setTimeout(() => {
|
|
219
|
+
modelRetryTimer = null;
|
|
220
|
+
log.info(`[dsh-agentone] model sync self-heal retry #${modelRetryAttempts}`);
|
|
221
|
+
syncPlatformModel(config, log)
|
|
222
|
+
.then(() => {
|
|
223
|
+
modelRetryAttempts = 0;
|
|
224
|
+
})
|
|
225
|
+
.catch(() => {
|
|
226
|
+
scheduleModelRetry(config, log);
|
|
227
|
+
});
|
|
228
|
+
}, delay);
|
|
229
|
+
modelRetryTimer.unref?.();
|
|
230
|
+
}
|
|
231
|
+
|
|
197
232
|
/** 转发 dsh plugin 子命令:desktop 走受管 pnpm,普通 dsh 走官方 CLI。 */
|
|
198
233
|
async function runPluginCli(args) {
|
|
199
234
|
if (desktopPluginCli) {
|
|
@@ -718,6 +753,9 @@ export function apply(ctx, config) {
|
|
|
718
753
|
});
|
|
719
754
|
}
|
|
720
755
|
ctx.effect(() => {
|
|
756
|
+
// 模块级重试定时器跨 apply 存活:重入先清,避免旧定时器引用旧 config
|
|
757
|
+
clearModelRetry();
|
|
758
|
+
modelRetryAttempts = 0;
|
|
721
759
|
const register = (path, handler) => ctx.webServer.register({ kind: 'exact', path, handler });
|
|
722
760
|
|
|
723
761
|
register('/agentone/', (request, response) => {
|
|
@@ -760,6 +798,14 @@ export function apply(ctx, config) {
|
|
|
760
798
|
if (userEmail) {
|
|
761
799
|
({ capabilities, skills, displayNames } = await cachedPlatformStatus(config, credentials));
|
|
762
800
|
}
|
|
801
|
+
// 失败自愈:模型同步残留错误时由轮询驱动后台重试(成功即清,
|
|
802
|
+
// 下次轮询状态页恢复);错误已被登录/手动同步清除时取消待定
|
|
803
|
+
// 重试并复位退避计数。
|
|
804
|
+
if (credentials.model_error) scheduleModelRetry(config, log);
|
|
805
|
+
else {
|
|
806
|
+
clearModelRetry();
|
|
807
|
+
modelRetryAttempts = 0;
|
|
808
|
+
}
|
|
763
809
|
}
|
|
764
810
|
sendJson(response, 200, {
|
|
765
811
|
platform_url: config.platformUrl,
|
|
@@ -1357,6 +1403,8 @@ export function apply(ctx, config) {
|
|
|
1357
1403
|
return () => {
|
|
1358
1404
|
pendingStates.clear();
|
|
1359
1405
|
pendingCliAuth.clear();
|
|
1406
|
+
clearModelRetry();
|
|
1407
|
+
modelRetryAttempts = 0;
|
|
1360
1408
|
log.info('[dsh-agentone] routes unmounted');
|
|
1361
1409
|
};
|
|
1362
1410
|
}, 'dsh-agentone: http routes');
|
package/lib/store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// 平台凭据持久化:$DSH_HOME/agentone/credentials.json(0600,仅本机插件可读)。
|
|
2
|
-
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { copyFile, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
|
|
@@ -15,6 +15,26 @@ function credentialsPath(config) {
|
|
|
15
15
|
return join(storeDir(config), 'credentials.json');
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* 凭据更新互斥链:启动块、登录回调、手动同步、自愈重试可能在同一进程
|
|
20
|
+
* 内并发执行 read-merge-write(updateCredentials)。无锁时两个并发写会
|
|
21
|
+
* 互相丢补丁,且 writeFile 非原子——读到半截 JSON 会触发 loadCredentials
|
|
22
|
+
* 的容错把文件当损坏移开(登录态直接丢失)。这里把所有更新串行化,
|
|
23
|
+
* 写入改为 tmp + rename 原子替换,任一时刻磁盘上都是完整 JSON。
|
|
24
|
+
*/
|
|
25
|
+
let credentialsWriteChain = Promise.resolve();
|
|
26
|
+
|
|
27
|
+
function serializedWrite(config, writeFn) {
|
|
28
|
+
const next = credentialsWriteChain.then(writeFn, writeFn);
|
|
29
|
+
// 链上错误不阻断后续更新,也不进调用方的结果
|
|
30
|
+
credentialsWriteChain = next.catch(() => {});
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tmpName(path) {
|
|
35
|
+
return `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
/**
|
|
19
39
|
* 读取凭据。文件损坏(截断/手改坏)不抛出:把坏文件移开为
|
|
20
40
|
* credentials.json.corrupt,返回 null 回到未登录态——否则 logout 也依赖
|
|
@@ -40,21 +60,32 @@ export async function loadCredentials(config) {
|
|
|
40
60
|
}
|
|
41
61
|
}
|
|
42
62
|
|
|
43
|
-
export
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
export function saveCredentials(config, credentials) {
|
|
64
|
+
return serializedWrite(config, async () => {
|
|
65
|
+
const dir = storeDir(config);
|
|
66
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
67
|
+
const path = credentialsPath(config);
|
|
68
|
+
const tmp = tmpName(path);
|
|
69
|
+
await writeFile(tmp, JSON.stringify(credentials, null, 2) + '\n', { mode: 0o600, flag: 'w' });
|
|
70
|
+
await rename(tmp, path);
|
|
49
71
|
});
|
|
50
72
|
}
|
|
51
73
|
|
|
52
74
|
/** 读取-合并-写回:模型同步结果等增量字段落盘。 */
|
|
53
|
-
export
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
export function updateCredentials(config, patch) {
|
|
76
|
+
return serializedWrite(config, async () => {
|
|
77
|
+
const current = (await loadCredentials(config)) || {};
|
|
78
|
+
const path = credentialsPath(config);
|
|
79
|
+
const merged = { ...current, ...patch };
|
|
80
|
+
const dir = storeDir(config);
|
|
81
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
82
|
+
const tmp = tmpName(path);
|
|
83
|
+
await writeFile(tmp, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600, flag: 'w' });
|
|
84
|
+
await rename(tmp, path);
|
|
85
|
+
return merged;
|
|
86
|
+
});
|
|
56
87
|
}
|
|
57
88
|
|
|
58
|
-
export
|
|
59
|
-
|
|
89
|
+
export function clearCredentials(config) {
|
|
90
|
+
return serializedWrite(config, () => rm(credentialsPath(config), { force: true }));
|
|
60
91
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agentone",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "AgentOne
|
|
3
|
+
"version": "0.5.18",
|
|
4
|
+
"description": "AgentOne 平台集成插件:飞书登录、平台模型(同步失败自愈重试)、SkillHub 技能(含升级检测与一键升级)、套餐申请、插件管理与自助升级、lark-cli / ccpg-cli 探测(请求超时与错误码透传、令牌轮换 single-flight、凭据损坏容错与并发安全写入、状态页 UX 增强)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|