@wenbin_wb/dsh-bridge 2.11.0 → 2.11.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/CHANGELOG.md +36 -0
- package/client/client.js +57 -19
- package/client/index.js +21 -9
- package/client/mobile-styles.js +45 -11
- package/docs/release-process.md +6 -1
- package/docs/telegram-usage.md +1 -1
- package/lib/feishu/node.js +439 -439
- package/lib/index.js +37 -12
- package/lib/platform/base.js +147 -147
- package/lib/platform/dsh-storage.js +117 -117
- package/lib/platform/index.js +10 -10
- package/lib/platform/message-split.js +229 -229
- package/lib/platform/stream-slices.js +21 -21
- package/lib/restart-helper.mjs +17 -3
- package/lib/telegram/node.js +322 -322
- package/lib/wechat/gateway.js +986 -986
- package/lib/wechat/media.js +285 -285
- package/lib/wechat/node.js +352 -352
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { get as httpsGet } from 'node:https';
|
|
|
10
10
|
import { networkInterfaces, homedir, totalmem, freemem, cpus, loadavg, platform, arch, release, hostname, uptime } from 'node:os';
|
|
11
11
|
import { join, dirname, basename, resolve, normalize } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import { readFileSync, appendFileSync, existsSync, realpathSync } from 'node:fs';
|
|
13
|
+
import { readFileSync, appendFileSync, existsSync, realpathSync, mkdirSync } from 'node:fs';
|
|
14
14
|
import { readFile, writeFile, mkdir, unlink, readdir, stat, access } from 'node:fs/promises';
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
16
16
|
import QRCode from 'qrcode';
|
|
@@ -278,12 +278,18 @@ function sanitizeProxyHeaders(headers, extra) {
|
|
|
278
278
|
* @returns {string} 单元名,取不到返回空串
|
|
279
279
|
*/
|
|
280
280
|
export function parseSystemdUnit(cgroup) {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
281
|
+
const lines = String(cgroup || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
282
|
+
for (const line of lines) {
|
|
283
|
+
const parts = line.split('/').map((p) => p.trim()).filter(Boolean);
|
|
284
|
+
// 只认**最内层**组件:若最内层不是 .service(例如 .scope、或 docker/k8s 的容器目录),
|
|
285
|
+
// 说明本进程不是被某个 service 直接托管,绝不能向上游误取祖先单元
|
|
286
|
+
// (否则会把 docker.service / cron.service 之类重启掉)。
|
|
287
|
+
const innermost = parts[parts.length - 1] ?? '';
|
|
288
|
+
if (innermost.endsWith('.service')) {
|
|
289
|
+
return innermost;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return '';
|
|
287
293
|
}
|
|
288
294
|
|
|
289
295
|
/**
|
|
@@ -304,8 +310,8 @@ export function detectSupervisor({ env = process.env, cgroup = '' } = {}) {
|
|
|
304
310
|
const systemdMarked = Boolean(env.INVOCATION_ID || env.JOURNAL_STREAM) || text.includes('.slice');
|
|
305
311
|
const unit = systemdMarked ? parseSystemdUnit(text) : '';
|
|
306
312
|
if (unit) {
|
|
307
|
-
const
|
|
308
|
-
return { kind: 'systemd', scope:
|
|
313
|
+
const isUser = text.includes('user.slice') || text.includes('/user@');
|
|
314
|
+
return { kind: 'systemd', scope: isUser ? 'user' : 'system', unit, reason: 'cgroup 检出 systemd 单元' };
|
|
309
315
|
}
|
|
310
316
|
return { kind: 'self', reason: '未检出托管器(无 DSH_DAEMON/PM2_HOME,也不在 systemd 单元内)' };
|
|
311
317
|
}
|
|
@@ -1379,6 +1385,7 @@ class BridgeService {
|
|
|
1379
1385
|
const dshHome = env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
1380
1386
|
const logFile = join(dshHome, 'dsh-bridge', 'restart.log');
|
|
1381
1387
|
const childLog = join(dshHome, 'dsh-bridge', 'restart-child.log');
|
|
1388
|
+
try { mkdirSync(dirname(logFile), { recursive: true }); } catch { /* 日志目录不可写时忽略 */ }
|
|
1382
1389
|
const writeLog = (message) => {
|
|
1383
1390
|
try { appendFileSync(logFile, `[${new Date().toISOString()}] ${message}\n`); } catch { /* 日志不可写时忽略 */ }
|
|
1384
1391
|
};
|
|
@@ -1389,18 +1396,36 @@ class BridgeService {
|
|
|
1389
1396
|
|
|
1390
1397
|
if (plan.kind === 'systemd') {
|
|
1391
1398
|
const args = [...(plan.scope === 'user' ? ['--user'] : []), 'restart', '--no-block', plan.unit];
|
|
1399
|
+
const systemctlEnv = { ...env };
|
|
1400
|
+
if (process.platform === 'linux') {
|
|
1401
|
+
const defaultPaths = ['/usr/local/bin', '/usr/bin', '/bin', '/usr/local/sbin', '/usr/sbin', '/sbin'];
|
|
1402
|
+
const existingPath = systemctlEnv.PATH || '';
|
|
1403
|
+
const missingPaths = defaultPaths.filter((p) => !existingPath.split(':').includes(p));
|
|
1404
|
+
if (missingPaths.length > 0) {
|
|
1405
|
+
systemctlEnv.PATH = existingPath ? `${existingPath}:${missingPaths.join(':')}` : missingPaths.join(':');
|
|
1406
|
+
}
|
|
1407
|
+
if (plan.scope === 'user' && !systemctlEnv.XDG_RUNTIME_DIR && typeof process.getuid === 'function') {
|
|
1408
|
+
systemctlEnv.XDG_RUNTIME_DIR = `/run/user/${process.getuid()}`;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1392
1411
|
// 关键:必须以 **systemctl 的退出码** 判定成败,而不是"派生成功"。
|
|
1393
1412
|
// `--no-block` 下 systemctl 只发请求就返回(实测往返 ~6ms),但单元名不存在会 rc=5、
|
|
1394
1413
|
// 权限不足会 rc=1;旧写法只看 'spawn' 事件 → 必然失败也返回 ok:true 再 exit(0),
|
|
1395
1414
|
// 而干净退出不满足 Restart=on-failure → DSH 直接下线且不自动拉起。
|
|
1396
1415
|
const outcome = await new Promise((resolve) => {
|
|
1397
1416
|
let settled = false;
|
|
1398
|
-
const
|
|
1417
|
+
const timer = setTimeout(() => settle({ error: new Error('systemctl 执行超时(5s)') }), 5000);
|
|
1418
|
+
const settle = (value) => {
|
|
1419
|
+
if (!settled) {
|
|
1420
|
+
settled = true;
|
|
1421
|
+
clearTimeout(timer);
|
|
1422
|
+
resolve(value);
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1399
1425
|
try {
|
|
1400
|
-
const child = spawnImpl('systemctl', args, { detached: true, stdio: 'ignore', env });
|
|
1426
|
+
const child = spawnImpl('systemctl', args, { detached: true, stdio: 'ignore', env: systemctlEnv });
|
|
1401
1427
|
child.once?.('error', (err) => settle({ error: err }));
|
|
1402
1428
|
child.once?.('close', (code) => settle({ code }));
|
|
1403
|
-
child.unref?.();
|
|
1404
1429
|
} catch (err) {
|
|
1405
1430
|
settle({ error: err });
|
|
1406
1431
|
}
|
package/lib/platform/base.js
CHANGED
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
// dsh-bridge 平台抽象基类
|
|
2
|
-
//
|
|
3
|
-
// 定义 IM 平台适配器的统一接口。每个平台(微信/QQ/飞书/Telegram…)继承本类,
|
|
4
|
-
// 实现协议层(登录、收发消息、typing)。平台无关的会话桥逻辑在 ConversationBridge
|
|
5
|
-
// (lib/platform/conversation-bridge.js)中实现,通过 platform 注入到 bridge。
|
|
6
|
-
//
|
|
7
|
-
// 生命周期:constructor → start() → stop() → dispose()
|
|
8
|
-
// 消息抽象:sendText / sendTyping / sendMedia(由子类实现)
|
|
9
|
-
|
|
10
|
-
export class Platform {
|
|
11
|
-
/**
|
|
12
|
-
* @param {object} opts
|
|
13
|
-
* @param {object} opts.ctx Cordis 上下文
|
|
14
|
-
* @param {object} opts.logger 日志器
|
|
15
|
-
* @param {object} [opts.config] 已持久化的平台配置(凭证等)
|
|
16
|
-
* @param {(patch: object) => (void|Promise<void>)} [opts.onPersist] 主插件保存回调
|
|
17
|
-
* @param {import('./conversation-bridge.js').ConversationBridge} [opts.bridge] 会话桥实例
|
|
18
|
-
*/
|
|
19
|
-
constructor({ ctx, logger, config = {}, onPersist, bridge } = {}) {
|
|
20
|
-
this.ctx = ctx
|
|
21
|
-
this.logger = logger
|
|
22
|
-
this.config = { ...config }
|
|
23
|
-
this.onPersist = onPersist ?? (() => {})
|
|
24
|
-
this.bridge = bridge ?? null
|
|
25
|
-
|
|
26
|
-
// 平台标识(子类必须设置)
|
|
27
|
-
this.id = ''
|
|
28
|
-
this.name = ''
|
|
29
|
-
|
|
30
|
-
// 连接状态与账号:子类可能用 getter 覆盖(如委托给 gateway),
|
|
31
|
-
// 因此仅在未被子类覆盖时才初始化默认值。
|
|
32
|
-
if (!('status' in this)) this.status = 'idle'
|
|
33
|
-
if (!('accountId' in this)) this.accountId = null
|
|
34
|
-
|
|
35
|
-
// 扫码/登录的流式状态(RPC 轮询读取)
|
|
36
|
-
this.loginState = {
|
|
37
|
-
phase: 'idle', // idle | qr | scaned | confirmed | done | error
|
|
38
|
-
qrPayload: null, // 待渲染内容:dataURL 图片 或 二维码文本
|
|
39
|
-
qrKind: null, // 'img' | 'text'
|
|
40
|
-
error: null,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
this.disposers = []
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ---- 平台能力声明(子类可覆盖)----
|
|
47
|
-
|
|
48
|
-
get capabilities() {
|
|
49
|
-
return {
|
|
50
|
-
supportsGroup: false, // 是否支持群聊
|
|
51
|
-
supportsMedia: false, // 是否支持媒体收发
|
|
52
|
-
supportsVoice: false, // 是否支持语音
|
|
53
|
-
supportsTyping: false, // 是否支持 typing 状态
|
|
54
|
-
maxMessageChars: 2000, // 单条消息最大字符数
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
get configured() {
|
|
59
|
-
return false
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---- 生命周期(子类必须实现 start/stop;dispose 已提供默认实现)----
|
|
63
|
-
|
|
64
|
-
async start() {
|
|
65
|
-
throw new Error(`${this.id || 'platform'}: start() not implemented`)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async stop() {
|
|
69
|
-
throw new Error(`${this.id || 'platform'}: stop() not implemented`)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
dispose() {
|
|
73
|
-
for (const disposer of this.disposers) {
|
|
74
|
-
try { disposer() } catch { /* 忽略 */ }
|
|
75
|
-
}
|
|
76
|
-
this.disposers = []
|
|
77
|
-
this.bridge?.dispose?.()
|
|
78
|
-
this.bridge = null
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ---- 消息抽象(子类必须实现)----
|
|
82
|
-
|
|
83
|
-
async sendText(peerId, text, opts = {}) {
|
|
84
|
-
throw new Error(`${this.id || 'platform'}: sendText() not implemented`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async sendTyping(peerId, state) {
|
|
88
|
-
return Promise.resolve()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async sendMedia(peerId, media, opts = {}) {
|
|
92
|
-
throw new Error(`${this.id || 'platform'}: sendMedia() not implemented`)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---- 登录(子类必须实现 login;getLoginState 已提供默认)----
|
|
96
|
-
|
|
97
|
-
async login(opts = {}) {
|
|
98
|
-
throw new Error(`${this.id || 'platform'}: login() not implemented`)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
getLoginState() {
|
|
102
|
-
return { ...this.loginState }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ---- 状态汇总(供 RPC/UI 读取)----
|
|
106
|
-
|
|
107
|
-
getStatus() {
|
|
108
|
-
return {
|
|
109
|
-
id: this.id,
|
|
110
|
-
name: this.name,
|
|
111
|
-
status: this.status,
|
|
112
|
-
configured: this.configured,
|
|
113
|
-
accountId: this.accountId,
|
|
114
|
-
login: this.getLoginState(),
|
|
115
|
-
peerId: this.bridge?.peerId ?? null,
|
|
116
|
-
sessionId: this.bridge?.activeSessionId ?? null,
|
|
117
|
-
config: this.getEditableConfig?.(),
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** 可编辑配置(供 UI 设置面板读取);子类可覆盖返回具体字段。 */
|
|
122
|
-
getEditableConfig() {
|
|
123
|
-
return {}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ---- 工具 ----
|
|
127
|
-
|
|
128
|
-
setStatus(status) {
|
|
129
|
-
if (this.status === status) return
|
|
130
|
-
this.status = status
|
|
131
|
-
try {
|
|
132
|
-
this.ctx.emit?.(`${this.id}/status`, status)
|
|
133
|
-
} catch { /* emit 失败不致命 */ }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async persist(patch) {
|
|
137
|
-
try {
|
|
138
|
-
await this.onPersist(patch)
|
|
139
|
-
} catch (err) {
|
|
140
|
-
this.logger?.warn?.(`[dsh-bridge ${this.id}] persist failed: %s`, err?.message ?? err)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async destroy() {
|
|
145
|
-
this.dispose()
|
|
146
|
-
}
|
|
147
|
-
}
|
|
1
|
+
// dsh-bridge 平台抽象基类
|
|
2
|
+
//
|
|
3
|
+
// 定义 IM 平台适配器的统一接口。每个平台(微信/QQ/飞书/Telegram…)继承本类,
|
|
4
|
+
// 实现协议层(登录、收发消息、typing)。平台无关的会话桥逻辑在 ConversationBridge
|
|
5
|
+
// (lib/platform/conversation-bridge.js)中实现,通过 platform 注入到 bridge。
|
|
6
|
+
//
|
|
7
|
+
// 生命周期:constructor → start() → stop() → dispose()
|
|
8
|
+
// 消息抽象:sendText / sendTyping / sendMedia(由子类实现)
|
|
9
|
+
|
|
10
|
+
export class Platform {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {object} opts.ctx Cordis 上下文
|
|
14
|
+
* @param {object} opts.logger 日志器
|
|
15
|
+
* @param {object} [opts.config] 已持久化的平台配置(凭证等)
|
|
16
|
+
* @param {(patch: object) => (void|Promise<void>)} [opts.onPersist] 主插件保存回调
|
|
17
|
+
* @param {import('./conversation-bridge.js').ConversationBridge} [opts.bridge] 会话桥实例
|
|
18
|
+
*/
|
|
19
|
+
constructor({ ctx, logger, config = {}, onPersist, bridge } = {}) {
|
|
20
|
+
this.ctx = ctx
|
|
21
|
+
this.logger = logger
|
|
22
|
+
this.config = { ...config }
|
|
23
|
+
this.onPersist = onPersist ?? (() => {})
|
|
24
|
+
this.bridge = bridge ?? null
|
|
25
|
+
|
|
26
|
+
// 平台标识(子类必须设置)
|
|
27
|
+
this.id = ''
|
|
28
|
+
this.name = ''
|
|
29
|
+
|
|
30
|
+
// 连接状态与账号:子类可能用 getter 覆盖(如委托给 gateway),
|
|
31
|
+
// 因此仅在未被子类覆盖时才初始化默认值。
|
|
32
|
+
if (!('status' in this)) this.status = 'idle'
|
|
33
|
+
if (!('accountId' in this)) this.accountId = null
|
|
34
|
+
|
|
35
|
+
// 扫码/登录的流式状态(RPC 轮询读取)
|
|
36
|
+
this.loginState = {
|
|
37
|
+
phase: 'idle', // idle | qr | scaned | confirmed | done | error
|
|
38
|
+
qrPayload: null, // 待渲染内容:dataURL 图片 或 二维码文本
|
|
39
|
+
qrKind: null, // 'img' | 'text'
|
|
40
|
+
error: null,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.disposers = []
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---- 平台能力声明(子类可覆盖)----
|
|
47
|
+
|
|
48
|
+
get capabilities() {
|
|
49
|
+
return {
|
|
50
|
+
supportsGroup: false, // 是否支持群聊
|
|
51
|
+
supportsMedia: false, // 是否支持媒体收发
|
|
52
|
+
supportsVoice: false, // 是否支持语音
|
|
53
|
+
supportsTyping: false, // 是否支持 typing 状态
|
|
54
|
+
maxMessageChars: 2000, // 单条消息最大字符数
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get configured() {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- 生命周期(子类必须实现 start/stop;dispose 已提供默认实现)----
|
|
63
|
+
|
|
64
|
+
async start() {
|
|
65
|
+
throw new Error(`${this.id || 'platform'}: start() not implemented`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async stop() {
|
|
69
|
+
throw new Error(`${this.id || 'platform'}: stop() not implemented`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
dispose() {
|
|
73
|
+
for (const disposer of this.disposers) {
|
|
74
|
+
try { disposer() } catch { /* 忽略 */ }
|
|
75
|
+
}
|
|
76
|
+
this.disposers = []
|
|
77
|
+
this.bridge?.dispose?.()
|
|
78
|
+
this.bridge = null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- 消息抽象(子类必须实现)----
|
|
82
|
+
|
|
83
|
+
async sendText(peerId, text, opts = {}) {
|
|
84
|
+
throw new Error(`${this.id || 'platform'}: sendText() not implemented`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async sendTyping(peerId, state) {
|
|
88
|
+
return Promise.resolve()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async sendMedia(peerId, media, opts = {}) {
|
|
92
|
+
throw new Error(`${this.id || 'platform'}: sendMedia() not implemented`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- 登录(子类必须实现 login;getLoginState 已提供默认)----
|
|
96
|
+
|
|
97
|
+
async login(opts = {}) {
|
|
98
|
+
throw new Error(`${this.id || 'platform'}: login() not implemented`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getLoginState() {
|
|
102
|
+
return { ...this.loginState }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- 状态汇总(供 RPC/UI 读取)----
|
|
106
|
+
|
|
107
|
+
getStatus() {
|
|
108
|
+
return {
|
|
109
|
+
id: this.id,
|
|
110
|
+
name: this.name,
|
|
111
|
+
status: this.status,
|
|
112
|
+
configured: this.configured,
|
|
113
|
+
accountId: this.accountId,
|
|
114
|
+
login: this.getLoginState(),
|
|
115
|
+
peerId: this.bridge?.peerId ?? null,
|
|
116
|
+
sessionId: this.bridge?.activeSessionId ?? null,
|
|
117
|
+
config: this.getEditableConfig?.(),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 可编辑配置(供 UI 设置面板读取);子类可覆盖返回具体字段。 */
|
|
122
|
+
getEditableConfig() {
|
|
123
|
+
return {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---- 工具 ----
|
|
127
|
+
|
|
128
|
+
setStatus(status) {
|
|
129
|
+
if (this.status === status) return
|
|
130
|
+
this.status = status
|
|
131
|
+
try {
|
|
132
|
+
this.ctx.emit?.(`${this.id}/status`, status)
|
|
133
|
+
} catch { /* emit 失败不致命 */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async persist(patch) {
|
|
137
|
+
try {
|
|
138
|
+
await this.onPersist(patch)
|
|
139
|
+
} catch (err) {
|
|
140
|
+
this.logger?.warn?.(`[dsh-bridge ${this.id}] persist failed: %s`, err?.message ?? err)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async destroy() {
|
|
145
|
+
this.dispose()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
// DSH 私有存储读取(workspace.json / session_projcache.json 兜底)
|
|
2
|
-
// 自 conversation-bridge.js 拆出。策略:内存服务(workspaceRegistry)优先,
|
|
3
|
-
// 仅当内存服务缺失时才落盘读 DSH 存储;文件不存在时安全返回空值。
|
|
4
|
-
// 注意:不再以 ctx._mock 作为跳过依据——测试通过提供内存服务或注入 DSH_HOME 保持隔离。
|
|
5
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
6
|
-
import { join, basename } from 'node:path'
|
|
7
|
-
import { homedir } from 'node:os'
|
|
8
|
-
|
|
9
|
-
export function getArchivedSessionIds(ctx) {
|
|
10
|
-
const archived = new Set()
|
|
11
|
-
// 1. 尝试从 ctx.workspaceRegistry 内存服务读取
|
|
12
|
-
try {
|
|
13
|
-
const list = ctx?.workspaceRegistry?.archivedSessionIds
|
|
14
|
-
if (Array.isArray(list)) {
|
|
15
|
-
for (const id of list) {
|
|
16
|
-
if (id) archived.add(String(id))
|
|
17
|
-
}
|
|
18
|
-
return archived
|
|
19
|
-
}
|
|
20
|
-
} catch { /* ignore */ }
|
|
21
|
-
|
|
22
|
-
// 2. 尝试从 DSH workspace 存储文件($DSH_HOME/storages/workspace.json)读取兜底
|
|
23
|
-
{
|
|
24
|
-
try {
|
|
25
|
-
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
26
|
-
const wsFile = join(home, 'storages', 'workspace.json')
|
|
27
|
-
if (existsSync(wsFile)) {
|
|
28
|
-
const data = JSON.parse(readFileSync(wsFile, 'utf8'))
|
|
29
|
-
const fileArchived = data?.global?.archivedSessionIds
|
|
30
|
-
if (Array.isArray(fileArchived)) {
|
|
31
|
-
for (const id of fileArchived) {
|
|
32
|
-
if (id) archived.add(String(id))
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
} catch { /* ignore */ }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return archived
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 安全探测 ctx 上的非 inject 属性。
|
|
44
|
-
* 宿主 cordis 上下文对插件未在 inject 中声明的属性读取会直接抛错
|
|
45
|
-
* ('cannot get property "x" without inject'),因此内存注入点(测试夹具用)
|
|
46
|
-
* 必须经 try/catch 探测,绝不能让异常外溢到命令路径。
|
|
47
|
-
*/
|
|
48
|
-
function peekCtxProperty(ctx, key) {
|
|
49
|
-
if (!ctx) return undefined
|
|
50
|
-
try { return ctx[key] } catch { return undefined }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** 读取 DSH 官方持久化会话缓存元数据(标题、是否空白、创建时间等) */
|
|
54
|
-
export function getSessionProjCache(ctx) {
|
|
55
|
-
const injected = peekCtxProperty(ctx, 'sessionProjCache')
|
|
56
|
-
if (injected) return injected
|
|
57
|
-
try {
|
|
58
|
-
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
59
|
-
const cacheFile = join(home, 'storages', 'session_projcache.json')
|
|
60
|
-
if (existsSync(cacheFile)) {
|
|
61
|
-
const data = JSON.parse(readFileSync(cacheFile, 'utf8'))
|
|
62
|
-
return data?.tables?.sessions || {}
|
|
63
|
-
}
|
|
64
|
-
} catch { /* ignore */ }
|
|
65
|
-
return {}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 读取 DSH 官方注册的工作区列表及各自绑定的 sessionIds 列表 */
|
|
69
|
-
export async function getRegisteredWorkspaces(ctx) {
|
|
70
|
-
const workspaces = []
|
|
71
|
-
|
|
72
|
-
// 优先从内存服务获取
|
|
73
|
-
if (ctx?.workspaceRegistry) {
|
|
74
|
-
try {
|
|
75
|
-
const list = await ctx.workspaceRegistry.list?.()
|
|
76
|
-
if (Array.isArray(list)) {
|
|
77
|
-
for (const w of list) {
|
|
78
|
-
if (w && w.path) {
|
|
79
|
-
workspaces.push({
|
|
80
|
-
id: w.id || w.path,
|
|
81
|
-
path: w.path,
|
|
82
|
-
title: w.title || basename(w.path),
|
|
83
|
-
sessionIds: Array.isArray(w.sessionIds) ? [...w.sessionIds] : [],
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return workspaces
|
|
88
|
-
}
|
|
89
|
-
} catch { /* ignore */ }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// 兜底从 workspace.json 存储文件读取
|
|
93
|
-
{
|
|
94
|
-
try {
|
|
95
|
-
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
96
|
-
const wsFile = join(home, 'storages', 'workspace.json')
|
|
97
|
-
if (existsSync(wsFile)) {
|
|
98
|
-
const data = JSON.parse(readFileSync(wsFile, 'utf8'))
|
|
99
|
-
const wsIds = data?.global?.workspaceIds || Object.keys(data?.tables?.workspaces || {})
|
|
100
|
-
const table = data?.tables?.workspaces || {}
|
|
101
|
-
for (const wId of wsIds) {
|
|
102
|
-
const ws = table[wId]
|
|
103
|
-
if (ws && ws.path) {
|
|
104
|
-
workspaces.push({
|
|
105
|
-
id: wId,
|
|
106
|
-
path: ws.path,
|
|
107
|
-
title: ws.title || basename(ws.path),
|
|
108
|
-
sessionIds: Array.isArray(ws.sessionIds) ? [...ws.sessionIds] : [],
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} catch { /* ignore */ }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return workspaces
|
|
117
|
-
}
|
|
1
|
+
// DSH 私有存储读取(workspace.json / session_projcache.json 兜底)
|
|
2
|
+
// 自 conversation-bridge.js 拆出。策略:内存服务(workspaceRegistry)优先,
|
|
3
|
+
// 仅当内存服务缺失时才落盘读 DSH 存储;文件不存在时安全返回空值。
|
|
4
|
+
// 注意:不再以 ctx._mock 作为跳过依据——测试通过提供内存服务或注入 DSH_HOME 保持隔离。
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
6
|
+
import { join, basename } from 'node:path'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
|
|
9
|
+
export function getArchivedSessionIds(ctx) {
|
|
10
|
+
const archived = new Set()
|
|
11
|
+
// 1. 尝试从 ctx.workspaceRegistry 内存服务读取
|
|
12
|
+
try {
|
|
13
|
+
const list = ctx?.workspaceRegistry?.archivedSessionIds
|
|
14
|
+
if (Array.isArray(list)) {
|
|
15
|
+
for (const id of list) {
|
|
16
|
+
if (id) archived.add(String(id))
|
|
17
|
+
}
|
|
18
|
+
return archived
|
|
19
|
+
}
|
|
20
|
+
} catch { /* ignore */ }
|
|
21
|
+
|
|
22
|
+
// 2. 尝试从 DSH workspace 存储文件($DSH_HOME/storages/workspace.json)读取兜底
|
|
23
|
+
{
|
|
24
|
+
try {
|
|
25
|
+
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
26
|
+
const wsFile = join(home, 'storages', 'workspace.json')
|
|
27
|
+
if (existsSync(wsFile)) {
|
|
28
|
+
const data = JSON.parse(readFileSync(wsFile, 'utf8'))
|
|
29
|
+
const fileArchived = data?.global?.archivedSessionIds
|
|
30
|
+
if (Array.isArray(fileArchived)) {
|
|
31
|
+
for (const id of fileArchived) {
|
|
32
|
+
if (id) archived.add(String(id))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch { /* ignore */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return archived
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 安全探测 ctx 上的非 inject 属性。
|
|
44
|
+
* 宿主 cordis 上下文对插件未在 inject 中声明的属性读取会直接抛错
|
|
45
|
+
* ('cannot get property "x" without inject'),因此内存注入点(测试夹具用)
|
|
46
|
+
* 必须经 try/catch 探测,绝不能让异常外溢到命令路径。
|
|
47
|
+
*/
|
|
48
|
+
function peekCtxProperty(ctx, key) {
|
|
49
|
+
if (!ctx) return undefined
|
|
50
|
+
try { return ctx[key] } catch { return undefined }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 读取 DSH 官方持久化会话缓存元数据(标题、是否空白、创建时间等) */
|
|
54
|
+
export function getSessionProjCache(ctx) {
|
|
55
|
+
const injected = peekCtxProperty(ctx, 'sessionProjCache')
|
|
56
|
+
if (injected) return injected
|
|
57
|
+
try {
|
|
58
|
+
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
59
|
+
const cacheFile = join(home, 'storages', 'session_projcache.json')
|
|
60
|
+
if (existsSync(cacheFile)) {
|
|
61
|
+
const data = JSON.parse(readFileSync(cacheFile, 'utf8'))
|
|
62
|
+
return data?.tables?.sessions || {}
|
|
63
|
+
}
|
|
64
|
+
} catch { /* ignore */ }
|
|
65
|
+
return {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 读取 DSH 官方注册的工作区列表及各自绑定的 sessionIds 列表 */
|
|
69
|
+
export async function getRegisteredWorkspaces(ctx) {
|
|
70
|
+
const workspaces = []
|
|
71
|
+
|
|
72
|
+
// 优先从内存服务获取
|
|
73
|
+
if (ctx?.workspaceRegistry) {
|
|
74
|
+
try {
|
|
75
|
+
const list = await ctx.workspaceRegistry.list?.()
|
|
76
|
+
if (Array.isArray(list)) {
|
|
77
|
+
for (const w of list) {
|
|
78
|
+
if (w && w.path) {
|
|
79
|
+
workspaces.push({
|
|
80
|
+
id: w.id || w.path,
|
|
81
|
+
path: w.path,
|
|
82
|
+
title: w.title || basename(w.path),
|
|
83
|
+
sessionIds: Array.isArray(w.sessionIds) ? [...w.sessionIds] : [],
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return workspaces
|
|
88
|
+
}
|
|
89
|
+
} catch { /* ignore */ }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 兜底从 workspace.json 存储文件读取
|
|
93
|
+
{
|
|
94
|
+
try {
|
|
95
|
+
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
96
|
+
const wsFile = join(home, 'storages', 'workspace.json')
|
|
97
|
+
if (existsSync(wsFile)) {
|
|
98
|
+
const data = JSON.parse(readFileSync(wsFile, 'utf8'))
|
|
99
|
+
const wsIds = data?.global?.workspaceIds || Object.keys(data?.tables?.workspaces || {})
|
|
100
|
+
const table = data?.tables?.workspaces || {}
|
|
101
|
+
for (const wId of wsIds) {
|
|
102
|
+
const ws = table[wId]
|
|
103
|
+
if (ws && ws.path) {
|
|
104
|
+
workspaces.push({
|
|
105
|
+
id: wId,
|
|
106
|
+
path: ws.path,
|
|
107
|
+
title: ws.title || basename(ws.path),
|
|
108
|
+
sessionIds: Array.isArray(ws.sessionIds) ? [...ws.sessionIds] : [],
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch { /* ignore */ }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return workspaces
|
|
117
|
+
}
|
package/lib/platform/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// dsh-bridge 平台抽象层统一导出
|
|
2
|
-
//
|
|
3
|
-
// 提供多平台 IM 接入的基础设施:
|
|
4
|
-
// Platform 平台适配器基类(协议/连接/登录/收发消息)
|
|
5
|
-
// ConversationBridge 平台无关会话桥(白名单/会话/审批/命令/digest)
|
|
6
|
-
// PlatformManager 多平台注册与状态聚合
|
|
7
|
-
|
|
8
|
-
export { Platform } from './base.js'
|
|
9
|
-
export { ConversationBridge, conversationBridgeHelpers, textOfAssistantMessage } from './conversation-bridge.js'
|
|
10
|
-
export { PlatformManager } from './manager.js'
|
|
1
|
+
// dsh-bridge 平台抽象层统一导出
|
|
2
|
+
//
|
|
3
|
+
// 提供多平台 IM 接入的基础设施:
|
|
4
|
+
// Platform 平台适配器基类(协议/连接/登录/收发消息)
|
|
5
|
+
// ConversationBridge 平台无关会话桥(白名单/会话/审批/命令/digest)
|
|
6
|
+
// PlatformManager 多平台注册与状态聚合
|
|
7
|
+
|
|
8
|
+
export { Platform } from './base.js'
|
|
9
|
+
export { ConversationBridge, conversationBridgeHelpers, textOfAssistantMessage } from './conversation-bridge.js'
|
|
10
|
+
export { PlatformManager } from './manager.js'
|