dsh-mobile-web 0.1.1 → 0.1.3
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/LICENSE-APACHE +202 -202
- package/NOTICE +26 -26
- package/README.md +17 -0
- package/assets/mobile.bundle.js +16898 -15958
- package/assets/theme.css +3492 -3439
- package/client.js +468 -141
- package/mobile-src/UPSTREAM.md +58 -50
- package/package.json +1 -1
- package/src/host/assets.js +9 -9
- package/src/host/cleanup.js +380 -0
- package/src/host/config.js +6 -0
- package/src/host/core.js +445 -18
- package/src/host/devices.js +226 -226
- package/src/host/http.js +8 -8
- package/src/host/pages.js +514 -514
- package/src/host/rpc/index.js +32 -32
- package/src/host/rpc/push.js +58 -58
- package/src/host/rpc/settings.js +88 -88
- package/src/host/rpc/subagent.js +27 -27
- package/src/host/state.js +30 -30
package/src/host/rpc/index.js
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RPC 注册表:方法名 → handler。
|
|
3
|
-
*
|
|
4
|
-
* 迁移期与 `core.js` 里的 switch **并存**:注册表优先命中,未命中的继续走 switch。
|
|
5
|
-
* 这样每搬一个域,方法集合与方法语义都保持逐字不变(可拿
|
|
6
|
-
* `.dsh-scratch/rpc-diff.mjs` 把「switch case ∪ 注册表键」和基线方法表比一遍)。
|
|
7
|
-
*
|
|
8
|
-
* handler 签名统一为 `(payload, bag) => Promise<Result>`:
|
|
9
|
-
* · `payload` = RPC 的 payload
|
|
10
|
-
* · `bag` = 每请求上下文(当前只有 `{ deviceId }`)
|
|
11
|
-
* · 返回 = `{ ok: true, value }` 或 `{ ok: false, error: { code, message, ... } }`
|
|
12
|
-
*
|
|
13
|
-
* 这里**不捕获异常**:抛错仍由 `handleRpc` 原有的 try/catch 包成错误信封,
|
|
14
|
-
* 免得迁移顺带改掉错误码语义。
|
|
15
|
-
*/
|
|
16
|
-
export function createRpcRegistry({ domains = [] } = {}) {
|
|
17
|
-
const table = new Map()
|
|
18
|
-
for (const domain of domains) {
|
|
19
|
-
if (!domain || typeof domain !== 'object') throw new Error('RPC 域必须是对象字面量')
|
|
20
|
-
for (const [method, handler] of Object.entries(domain)) {
|
|
21
|
-
if (typeof handler !== 'function') throw new Error(`RPC handler 不是函数:${method}`)
|
|
22
|
-
if (table.has(method)) throw new Error(`重复注册的 RPC 方法:${method}`)
|
|
23
|
-
table.set(method, handler)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
get: (method) => table.get(method),
|
|
28
|
-
has: (method) => table.has(method),
|
|
29
|
-
names: () => [...table.keys()].sort(),
|
|
30
|
-
size: () => table.size,
|
|
31
|
-
}
|
|
32
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* RPC 注册表:方法名 → handler。
|
|
3
|
+
*
|
|
4
|
+
* 迁移期与 `core.js` 里的 switch **并存**:注册表优先命中,未命中的继续走 switch。
|
|
5
|
+
* 这样每搬一个域,方法集合与方法语义都保持逐字不变(可拿
|
|
6
|
+
* `.dsh-scratch/rpc-diff.mjs` 把「switch case ∪ 注册表键」和基线方法表比一遍)。
|
|
7
|
+
*
|
|
8
|
+
* handler 签名统一为 `(payload, bag) => Promise<Result>`:
|
|
9
|
+
* · `payload` = RPC 的 payload
|
|
10
|
+
* · `bag` = 每请求上下文(当前只有 `{ deviceId }`)
|
|
11
|
+
* · 返回 = `{ ok: true, value }` 或 `{ ok: false, error: { code, message, ... } }`
|
|
12
|
+
*
|
|
13
|
+
* 这里**不捕获异常**:抛错仍由 `handleRpc` 原有的 try/catch 包成错误信封,
|
|
14
|
+
* 免得迁移顺带改掉错误码语义。
|
|
15
|
+
*/
|
|
16
|
+
export function createRpcRegistry({ domains = [] } = {}) {
|
|
17
|
+
const table = new Map()
|
|
18
|
+
for (const domain of domains) {
|
|
19
|
+
if (!domain || typeof domain !== 'object') throw new Error('RPC 域必须是对象字面量')
|
|
20
|
+
for (const [method, handler] of Object.entries(domain)) {
|
|
21
|
+
if (typeof handler !== 'function') throw new Error(`RPC handler 不是函数:${method}`)
|
|
22
|
+
if (table.has(method)) throw new Error(`重复注册的 RPC 方法:${method}`)
|
|
23
|
+
table.set(method, handler)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
get: (method) => table.get(method),
|
|
28
|
+
has: (method) => table.has(method),
|
|
29
|
+
names: () => [...table.keys()].sort(),
|
|
30
|
+
size: () => table.size,
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/host/rpc/push.js
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* push.* 域(3 个方法):通知配置的读写 + 订阅开关(订阅目前是空实现,保留是为了
|
|
3
|
-
* 让 bundle 的调用不报 not-supported)。
|
|
4
|
-
*
|
|
5
|
-
* 从 `core.js` 搬出。`notifyConfig` 是 core 里的可变状态(`let`),所以这里不拿值,
|
|
6
|
-
* 走 `getNotifyConfig()` / `setNotifyConfig()` 访问器;其余依赖都由 core 注入。
|
|
7
|
-
*/
|
|
8
|
-
export function createPushRpc({ getNotifyConfig, setNotifyConfig, deliverNotify, notifyChannelsView, saveNotifyConfig }) {
|
|
9
|
-
return {
|
|
10
|
-
'push.config': (payload) => {
|
|
11
|
-
const body = payload || {}
|
|
12
|
-
if (body.test === true) {
|
|
13
|
-
deliverNotify(getNotifyConfig(), { title: 'dsh 掌上 · 测试通知', body: '收到这条说明推送通道工作正常。' }).catch(() => {})
|
|
14
|
-
return { ok: true, value: { sent: true } }
|
|
15
|
-
}
|
|
16
|
-
if (body.get === true) {
|
|
17
|
-
const c = getNotifyConfig()
|
|
18
|
-
return {
|
|
19
|
-
ok: true,
|
|
20
|
-
value: {
|
|
21
|
-
turnThresholdMs: c.turnThresholdMs,
|
|
22
|
-
turnCooldownMs: c.turnCooldownMs,
|
|
23
|
-
hideDetails: c.hideDetails === true,
|
|
24
|
-
kinds: {
|
|
25
|
-
jobs: !!(c.kinds && c.kinds.jobs === true),
|
|
26
|
-
todo: !(c.kinds && c.kinds.todo === false),
|
|
27
|
-
turns: !!(c.kinds && c.kinds.turns === true),
|
|
28
|
-
},
|
|
29
|
-
vapidPublicKey: '',
|
|
30
|
-
channels: notifyChannelsView(c.channels),
|
|
31
|
-
},
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
const patch = body.set
|
|
35
|
-
if (!patch || typeof patch !== 'object') {
|
|
36
|
-
return { ok: false, error: { code: 'bad-request', message: '缺少配置内容' } }
|
|
37
|
-
}
|
|
38
|
-
const current = getNotifyConfig()
|
|
39
|
-
const next = { ...current }
|
|
40
|
-
if (typeof patch.turnThresholdMs === 'number' && Number.isFinite(patch.turnThresholdMs)) next.turnThresholdMs = Math.max(0, Math.round(patch.turnThresholdMs))
|
|
41
|
-
if (typeof patch.turnCooldownMs === 'number' && Number.isFinite(patch.turnCooldownMs)) next.turnCooldownMs = Math.max(0, Math.round(patch.turnCooldownMs))
|
|
42
|
-
if (typeof patch.hideDetails === 'boolean') next.hideDetails = patch.hideDetails
|
|
43
|
-
if (patch.kinds && typeof patch.kinds === 'object') next.kinds = { ...(current.kinds || {}), ...patch.kinds }
|
|
44
|
-
if (patch.channels && typeof patch.channels === 'object') {
|
|
45
|
-
const merged = { ...(current.channels || {}) }
|
|
46
|
-
for (const [k, v] of Object.entries(patch.channels)) {
|
|
47
|
-
if (v && typeof v === 'object') merged[k] = { ...(merged[k] || {}), ...v }
|
|
48
|
-
}
|
|
49
|
-
next.channels = merged
|
|
50
|
-
}
|
|
51
|
-
setNotifyConfig(next)
|
|
52
|
-
saveNotifyConfig(next)
|
|
53
|
-
return { ok: true, value: { stored: true } }
|
|
54
|
-
},
|
|
55
|
-
'push.subscribe': async () => ({ ok: true, value: { ok: true } }),
|
|
56
|
-
'push.unsubscribe': async () => ({ ok: true, value: { ok: true } }),
|
|
57
|
-
}
|
|
58
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* push.* 域(3 个方法):通知配置的读写 + 订阅开关(订阅目前是空实现,保留是为了
|
|
3
|
+
* 让 bundle 的调用不报 not-supported)。
|
|
4
|
+
*
|
|
5
|
+
* 从 `core.js` 搬出。`notifyConfig` 是 core 里的可变状态(`let`),所以这里不拿值,
|
|
6
|
+
* 走 `getNotifyConfig()` / `setNotifyConfig()` 访问器;其余依赖都由 core 注入。
|
|
7
|
+
*/
|
|
8
|
+
export function createPushRpc({ getNotifyConfig, setNotifyConfig, deliverNotify, notifyChannelsView, saveNotifyConfig }) {
|
|
9
|
+
return {
|
|
10
|
+
'push.config': (payload) => {
|
|
11
|
+
const body = payload || {}
|
|
12
|
+
if (body.test === true) {
|
|
13
|
+
deliverNotify(getNotifyConfig(), { title: 'dsh 掌上 · 测试通知', body: '收到这条说明推送通道工作正常。' }).catch(() => {})
|
|
14
|
+
return { ok: true, value: { sent: true } }
|
|
15
|
+
}
|
|
16
|
+
if (body.get === true) {
|
|
17
|
+
const c = getNotifyConfig()
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
value: {
|
|
21
|
+
turnThresholdMs: c.turnThresholdMs,
|
|
22
|
+
turnCooldownMs: c.turnCooldownMs,
|
|
23
|
+
hideDetails: c.hideDetails === true,
|
|
24
|
+
kinds: {
|
|
25
|
+
jobs: !!(c.kinds && c.kinds.jobs === true),
|
|
26
|
+
todo: !(c.kinds && c.kinds.todo === false),
|
|
27
|
+
turns: !!(c.kinds && c.kinds.turns === true),
|
|
28
|
+
},
|
|
29
|
+
vapidPublicKey: '',
|
|
30
|
+
channels: notifyChannelsView(c.channels),
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const patch = body.set
|
|
35
|
+
if (!patch || typeof patch !== 'object') {
|
|
36
|
+
return { ok: false, error: { code: 'bad-request', message: '缺少配置内容' } }
|
|
37
|
+
}
|
|
38
|
+
const current = getNotifyConfig()
|
|
39
|
+
const next = { ...current }
|
|
40
|
+
if (typeof patch.turnThresholdMs === 'number' && Number.isFinite(patch.turnThresholdMs)) next.turnThresholdMs = Math.max(0, Math.round(patch.turnThresholdMs))
|
|
41
|
+
if (typeof patch.turnCooldownMs === 'number' && Number.isFinite(patch.turnCooldownMs)) next.turnCooldownMs = Math.max(0, Math.round(patch.turnCooldownMs))
|
|
42
|
+
if (typeof patch.hideDetails === 'boolean') next.hideDetails = patch.hideDetails
|
|
43
|
+
if (patch.kinds && typeof patch.kinds === 'object') next.kinds = { ...(current.kinds || {}), ...patch.kinds }
|
|
44
|
+
if (patch.channels && typeof patch.channels === 'object') {
|
|
45
|
+
const merged = { ...(current.channels || {}) }
|
|
46
|
+
for (const [k, v] of Object.entries(patch.channels)) {
|
|
47
|
+
if (v && typeof v === 'object') merged[k] = { ...(merged[k] || {}), ...v }
|
|
48
|
+
}
|
|
49
|
+
next.channels = merged
|
|
50
|
+
}
|
|
51
|
+
setNotifyConfig(next)
|
|
52
|
+
saveNotifyConfig(next)
|
|
53
|
+
return { ok: true, value: { stored: true } }
|
|
54
|
+
},
|
|
55
|
+
'push.subscribe': async () => ({ ok: true, value: { ok: true } }),
|
|
56
|
+
'push.unsubscribe': async () => ({ ok: true, value: { ok: true } }),
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/host/rpc/settings.js
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* settings.* 域(3 个方法):宿主设置页的只读视图 + 手机端唯一可写的「外观偏好」。
|
|
3
|
-
*
|
|
4
|
-
* 从 `core.js` 的 switch 原样搬出。依赖由 `core.js` 一次性注入:
|
|
5
|
-
* · `getService(name)` —— 取宿主服务(这里是 `settings`)
|
|
6
|
-
* · `devices` —— 设备表(外观偏好按设备存)
|
|
7
|
-
* · `persistDevices()` —— 落盘设备表
|
|
8
|
-
* handler 的第二个参数 `bag` 是每请求上下文,这里只用到 `bag.deviceId`。
|
|
9
|
-
*/
|
|
10
|
-
export function createSettingsRpc({ getService, devices, persistDevices }) {
|
|
11
|
-
/**
|
|
12
|
-
* settings.read / settings.describe 共用:返回 namespaces 列表,
|
|
13
|
-
* 并把**本设备**自己选的外观偏好回填进 ui-theme。
|
|
14
|
-
*/
|
|
15
|
-
function readSettings(payload, bag) {
|
|
16
|
-
const settings = getService('settings')
|
|
17
|
-
if (!settings || typeof settings.describe !== 'function') {
|
|
18
|
-
return { ok: true, value: { writable: false, hasDocument: false, namespaces: [] } }
|
|
19
|
-
}
|
|
20
|
-
let list
|
|
21
|
-
try {
|
|
22
|
-
list = settings.describe({ redactSecrets: true })
|
|
23
|
-
} catch {
|
|
24
|
-
list = []
|
|
25
|
-
}
|
|
26
|
-
const namespaces = (Array.isArray(list) ? list : []).map((d) => ({
|
|
27
|
-
ns: d.ns,
|
|
28
|
-
schema: d.schema,
|
|
29
|
-
value: d.value,
|
|
30
|
-
...(d.base !== undefined ? { base: d.base } : {}),
|
|
31
|
-
...(d.user !== undefined ? { user: d.user } : {}),
|
|
32
|
-
applies: d.applies || 'live',
|
|
33
|
-
secrets: Array.isArray(d.secrets) ? d.secrets : [],
|
|
34
|
-
revision: d.revision,
|
|
35
|
-
}))
|
|
36
|
-
// 外观偏好按设备独立:bundle 每次启动都会读 ui-theme.preference 并应用
|
|
37
|
-
// (Fr(value)),如果不覆盖,它就会用宿主那份跨端同步值把手机上刚设的
|
|
38
|
-
// 深色顶掉 —— 表现就是「设成深色,刷新又变浅色」。
|
|
39
|
-
// 这里把该设备自己的选择回填进去,手机之间/桌面端互不影响。
|
|
40
|
-
const settingsDevice = bag && typeof bag.deviceId === 'string'
|
|
41
|
-
? devices.get(bag.deviceId)
|
|
42
|
-
: undefined
|
|
43
|
-
const deviceTheme = settingsDevice && typeof settingsDevice.theme === 'string' ? settingsDevice.theme : ''
|
|
44
|
-
if (deviceTheme === 'dark' || deviceTheme === 'light' || deviceTheme === 'system') {
|
|
45
|
-
for (const entry of namespaces) {
|
|
46
|
-
if (entry.ns !== 'ui-theme') continue
|
|
47
|
-
const value = entry.value && typeof entry.value === 'object' ? { ...entry.value } : {}
|
|
48
|
-
value.preference = deviceTheme
|
|
49
|
-
entry.value = value
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return { ok: true, value: { writable: false, hasDocument: false, namespaces } }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
'settings.read': readSettings,
|
|
57
|
-
'settings.describe': readSettings,
|
|
58
|
-
'settings.mutate': (payload, bag) => {
|
|
59
|
-
// 手机端只开放「外观偏好」这一项,而且是**按设备**存:
|
|
60
|
-
// · 写宿主 ui-theme 是跨端同步设置,手机上换深色会把桌面 GUI 一起改掉;
|
|
61
|
-
// · 用户预期是「手机自己的主题跟着手机走」,所以存进设备表,
|
|
62
|
-
// 启动时再由上面的 settings.read 回填给 bundle。
|
|
63
|
-
const targetId = bag && typeof bag.deviceId === 'string' ? bag.deviceId : ''
|
|
64
|
-
const target = devices.get(targetId)
|
|
65
|
-
if (!target) return { ok: false, error: { code: 'unpaired', message: '设备未配对' } }
|
|
66
|
-
const ns = typeof payload?.ns === 'string' ? payload.ns : ''
|
|
67
|
-
if (ns !== 'ui-theme') {
|
|
68
|
-
return { ok: false, error: { code: 'forbidden', message: '该设置项不允许在手机端修改' } }
|
|
69
|
-
}
|
|
70
|
-
const ops = Array.isArray(payload?.ops) ? payload.ops : []
|
|
71
|
-
let nextTheme = ''
|
|
72
|
-
for (const op of ops) {
|
|
73
|
-
if (!op || op.op !== 'set' || !Array.isArray(op.path) || op.path.length !== 1 || op.path[0] !== 'preference') {
|
|
74
|
-
return { ok: false, error: { code: 'bad-request', message: '只支持设置外观偏好' } }
|
|
75
|
-
}
|
|
76
|
-
if (op.value !== 'dark' && op.value !== 'light' && op.value !== 'system') {
|
|
77
|
-
return { ok: false, error: { code: 'bad-request', message: '外观偏好只能是 dark / light / system' } }
|
|
78
|
-
}
|
|
79
|
-
nextTheme = op.value
|
|
80
|
-
}
|
|
81
|
-
if (nextTheme !== '' && target.theme !== nextTheme) {
|
|
82
|
-
target.theme = nextTheme
|
|
83
|
-
persistDevices()
|
|
84
|
-
}
|
|
85
|
-
return { ok: true, value: { ok: true } }
|
|
86
|
-
},
|
|
87
|
-
}
|
|
88
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* settings.* 域(3 个方法):宿主设置页的只读视图 + 手机端唯一可写的「外观偏好」。
|
|
3
|
+
*
|
|
4
|
+
* 从 `core.js` 的 switch 原样搬出。依赖由 `core.js` 一次性注入:
|
|
5
|
+
* · `getService(name)` —— 取宿主服务(这里是 `settings`)
|
|
6
|
+
* · `devices` —— 设备表(外观偏好按设备存)
|
|
7
|
+
* · `persistDevices()` —— 落盘设备表
|
|
8
|
+
* handler 的第二个参数 `bag` 是每请求上下文,这里只用到 `bag.deviceId`。
|
|
9
|
+
*/
|
|
10
|
+
export function createSettingsRpc({ getService, devices, persistDevices }) {
|
|
11
|
+
/**
|
|
12
|
+
* settings.read / settings.describe 共用:返回 namespaces 列表,
|
|
13
|
+
* 并把**本设备**自己选的外观偏好回填进 ui-theme。
|
|
14
|
+
*/
|
|
15
|
+
function readSettings(payload, bag) {
|
|
16
|
+
const settings = getService('settings')
|
|
17
|
+
if (!settings || typeof settings.describe !== 'function') {
|
|
18
|
+
return { ok: true, value: { writable: false, hasDocument: false, namespaces: [] } }
|
|
19
|
+
}
|
|
20
|
+
let list
|
|
21
|
+
try {
|
|
22
|
+
list = settings.describe({ redactSecrets: true })
|
|
23
|
+
} catch {
|
|
24
|
+
list = []
|
|
25
|
+
}
|
|
26
|
+
const namespaces = (Array.isArray(list) ? list : []).map((d) => ({
|
|
27
|
+
ns: d.ns,
|
|
28
|
+
schema: d.schema,
|
|
29
|
+
value: d.value,
|
|
30
|
+
...(d.base !== undefined ? { base: d.base } : {}),
|
|
31
|
+
...(d.user !== undefined ? { user: d.user } : {}),
|
|
32
|
+
applies: d.applies || 'live',
|
|
33
|
+
secrets: Array.isArray(d.secrets) ? d.secrets : [],
|
|
34
|
+
revision: d.revision,
|
|
35
|
+
}))
|
|
36
|
+
// 外观偏好按设备独立:bundle 每次启动都会读 ui-theme.preference 并应用
|
|
37
|
+
// (Fr(value)),如果不覆盖,它就会用宿主那份跨端同步值把手机上刚设的
|
|
38
|
+
// 深色顶掉 —— 表现就是「设成深色,刷新又变浅色」。
|
|
39
|
+
// 这里把该设备自己的选择回填进去,手机之间/桌面端互不影响。
|
|
40
|
+
const settingsDevice = bag && typeof bag.deviceId === 'string'
|
|
41
|
+
? devices.get(bag.deviceId)
|
|
42
|
+
: undefined
|
|
43
|
+
const deviceTheme = settingsDevice && typeof settingsDevice.theme === 'string' ? settingsDevice.theme : ''
|
|
44
|
+
if (deviceTheme === 'dark' || deviceTheme === 'light' || deviceTheme === 'system') {
|
|
45
|
+
for (const entry of namespaces) {
|
|
46
|
+
if (entry.ns !== 'ui-theme') continue
|
|
47
|
+
const value = entry.value && typeof entry.value === 'object' ? { ...entry.value } : {}
|
|
48
|
+
value.preference = deviceTheme
|
|
49
|
+
entry.value = value
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, value: { writable: false, hasDocument: false, namespaces } }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
'settings.read': readSettings,
|
|
57
|
+
'settings.describe': readSettings,
|
|
58
|
+
'settings.mutate': (payload, bag) => {
|
|
59
|
+
// 手机端只开放「外观偏好」这一项,而且是**按设备**存:
|
|
60
|
+
// · 写宿主 ui-theme 是跨端同步设置,手机上换深色会把桌面 GUI 一起改掉;
|
|
61
|
+
// · 用户预期是「手机自己的主题跟着手机走」,所以存进设备表,
|
|
62
|
+
// 启动时再由上面的 settings.read 回填给 bundle。
|
|
63
|
+
const targetId = bag && typeof bag.deviceId === 'string' ? bag.deviceId : ''
|
|
64
|
+
const target = devices.get(targetId)
|
|
65
|
+
if (!target) return { ok: false, error: { code: 'unpaired', message: '设备未配对' } }
|
|
66
|
+
const ns = typeof payload?.ns === 'string' ? payload.ns : ''
|
|
67
|
+
if (ns !== 'ui-theme') {
|
|
68
|
+
return { ok: false, error: { code: 'forbidden', message: '该设置项不允许在手机端修改' } }
|
|
69
|
+
}
|
|
70
|
+
const ops = Array.isArray(payload?.ops) ? payload.ops : []
|
|
71
|
+
let nextTheme = ''
|
|
72
|
+
for (const op of ops) {
|
|
73
|
+
if (!op || op.op !== 'set' || !Array.isArray(op.path) || op.path.length !== 1 || op.path[0] !== 'preference') {
|
|
74
|
+
return { ok: false, error: { code: 'bad-request', message: '只支持设置外观偏好' } }
|
|
75
|
+
}
|
|
76
|
+
if (op.value !== 'dark' && op.value !== 'light' && op.value !== 'system') {
|
|
77
|
+
return { ok: false, error: { code: 'bad-request', message: '外观偏好只能是 dark / light / system' } }
|
|
78
|
+
}
|
|
79
|
+
nextTheme = op.value
|
|
80
|
+
}
|
|
81
|
+
if (nextTheme !== '' && target.theme !== nextTheme) {
|
|
82
|
+
target.theme = nextTheme
|
|
83
|
+
persistDevices()
|
|
84
|
+
}
|
|
85
|
+
return { ok: true, value: { ok: true } }
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/host/rpc/subagent.js
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* subagent.* 域(1 个方法):列出某会话的子代理。
|
|
3
|
-
*
|
|
4
|
-
* 从 `core.js` 搬出。顺手登记「子代理 id → 父会话 id」的缓存(`recordSubagentParents`
|
|
5
|
-
* 是 core 里那张 `subagentParent` 表的写入口,面板点开某行看聊天时要用父地址读)。
|
|
6
|
-
*/
|
|
7
|
-
export function createSubagentRpc({ getService, recordSubagentParents }) {
|
|
8
|
-
return {
|
|
9
|
-
'subagent.list': async (payload) => {
|
|
10
|
-
const parentSessionId = typeof payload?.parentSessionId === 'string' ? payload.parentSessionId : ''
|
|
11
|
-
if (parentSessionId === '') return { ok: true, value: { entries: [], parentAvailable: false } }
|
|
12
|
-
const subagents = getService('subagents')
|
|
13
|
-
if (!subagents || typeof subagents.listDescendants !== 'function') {
|
|
14
|
-
return { ok: true, value: { entries: [], parentAvailable: false } }
|
|
15
|
-
}
|
|
16
|
-
try {
|
|
17
|
-
const rows = await subagents.listDescendants(parentSessionId, new AbortController().signal)
|
|
18
|
-
// 顺手登记「子代理 id → 父会话 id」:面板点开某行看聊天时要用父地址读,
|
|
19
|
-
// 这里记住了就免去反查(每行自带 parentId,本函数的入参就是它)。
|
|
20
|
-
recordSubagentParents(rows)
|
|
21
|
-
return { ok: true, value: { entries: rows, parentAvailable: rows.length > 0 } }
|
|
22
|
-
} catch {
|
|
23
|
-
return { ok: true, value: { entries: [], parentAvailable: false } }
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
}
|
|
27
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* subagent.* 域(1 个方法):列出某会话的子代理。
|
|
3
|
+
*
|
|
4
|
+
* 从 `core.js` 搬出。顺手登记「子代理 id → 父会话 id」的缓存(`recordSubagentParents`
|
|
5
|
+
* 是 core 里那张 `subagentParent` 表的写入口,面板点开某行看聊天时要用父地址读)。
|
|
6
|
+
*/
|
|
7
|
+
export function createSubagentRpc({ getService, recordSubagentParents }) {
|
|
8
|
+
return {
|
|
9
|
+
'subagent.list': async (payload) => {
|
|
10
|
+
const parentSessionId = typeof payload?.parentSessionId === 'string' ? payload.parentSessionId : ''
|
|
11
|
+
if (parentSessionId === '') return { ok: true, value: { entries: [], parentAvailable: false } }
|
|
12
|
+
const subagents = getService('subagents')
|
|
13
|
+
if (!subagents || typeof subagents.listDescendants !== 'function') {
|
|
14
|
+
return { ok: true, value: { entries: [], parentAvailable: false } }
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const rows = await subagents.listDescendants(parentSessionId, new AbortController().signal)
|
|
18
|
+
// 顺手登记「子代理 id → 父会话 id」:面板点开某行看聊天时要用父地址读,
|
|
19
|
+
// 这里记住了就免去反查(每行自带 parentId,本函数的入参就是它)。
|
|
20
|
+
recordSubagentParents(rows)
|
|
21
|
+
return { ok: true, value: { entries: rows, parentAvailable: rows.length > 0 } }
|
|
22
|
+
} catch {
|
|
23
|
+
return { ok: true, value: { entries: [], parentAvailable: false } }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/host/state.js
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 宿主共享状态容器(ctxBag 的第一层)。
|
|
3
|
-
*
|
|
4
|
-
* 这些是**运行时可变容器**,不是模块级全局:由 core 的 createHost 创建一次,
|
|
5
|
-
* 再以别名(devices/observers/...)交给闭包内的函数,以及后续搬出的 devices/sse
|
|
6
|
-
* 模块使用。搬出模块时只要把 `state` 传进去即可,不需要再靠闭包捕获。
|
|
7
|
-
*/
|
|
8
|
-
export function createHostState({ devices }) {
|
|
9
|
-
return {
|
|
10
|
-
// 设备表:deviceId -> { token, createdAt, lastSeenAt, ... }
|
|
11
|
-
devices,
|
|
12
|
-
// 当前待用的 6 位配对码(null = 没有有效码)
|
|
13
|
-
pairToken: null,
|
|
14
|
-
// 待处理:approval / question 的 rpcId 注册表(含 resolve)
|
|
15
|
-
pending: {
|
|
16
|
-
approvals: new Map(),
|
|
17
|
-
questions: new Map(),
|
|
18
|
-
byApprovalId: new Map(),
|
|
19
|
-
},
|
|
20
|
-
// SSE:设备正在看的会话、观察时间、follow 单例、流集合、每轮流式状态
|
|
21
|
-
observers: new Map(),
|
|
22
|
-
lastObservedByDevice: new Map(),
|
|
23
|
-
lastObserved: new Map(),
|
|
24
|
-
follows: new Map(),
|
|
25
|
-
streamStates: new Map(),
|
|
26
|
-
muxStreams: new Set(),
|
|
27
|
-
lastPersistedSeq: new Map(),
|
|
28
|
-
lastStep: new Map(),
|
|
29
|
-
}
|
|
30
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 宿主共享状态容器(ctxBag 的第一层)。
|
|
3
|
+
*
|
|
4
|
+
* 这些是**运行时可变容器**,不是模块级全局:由 core 的 createHost 创建一次,
|
|
5
|
+
* 再以别名(devices/observers/...)交给闭包内的函数,以及后续搬出的 devices/sse
|
|
6
|
+
* 模块使用。搬出模块时只要把 `state` 传进去即可,不需要再靠闭包捕获。
|
|
7
|
+
*/
|
|
8
|
+
export function createHostState({ devices }) {
|
|
9
|
+
return {
|
|
10
|
+
// 设备表:deviceId -> { token, createdAt, lastSeenAt, ... }
|
|
11
|
+
devices,
|
|
12
|
+
// 当前待用的 6 位配对码(null = 没有有效码)
|
|
13
|
+
pairToken: null,
|
|
14
|
+
// 待处理:approval / question 的 rpcId 注册表(含 resolve)
|
|
15
|
+
pending: {
|
|
16
|
+
approvals: new Map(),
|
|
17
|
+
questions: new Map(),
|
|
18
|
+
byApprovalId: new Map(),
|
|
19
|
+
},
|
|
20
|
+
// SSE:设备正在看的会话、观察时间、follow 单例、流集合、每轮流式状态
|
|
21
|
+
observers: new Map(),
|
|
22
|
+
lastObservedByDevice: new Map(),
|
|
23
|
+
lastObserved: new Map(),
|
|
24
|
+
follows: new Map(),
|
|
25
|
+
streamStates: new Map(),
|
|
26
|
+
muxStreams: new Set(),
|
|
27
|
+
lastPersistedSeq: new Map(),
|
|
28
|
+
lastStep: new Map(),
|
|
29
|
+
}
|
|
30
|
+
}
|