@twinsearth/w2m-dsh-plugin 3.0.0 → 3.2.0
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +38 -0
- package/README.md +16 -1
- package/codex/mcp-servers.toml +1 -1
- package/cordis.patch.yml +10 -0
- package/docs/acceptance/3.0.0/RELEASE-3000.json +3 -1
- package/docs/acceptance/3.1.0/RELEASE-3100.json +46 -0
- package/docs/acceptance/3.2.0/RELEASE-3200.json +47 -0
- package/lib/client.js +444 -0
- package/lib/contract.js +22 -0
- package/lib/queue-journal.js +15 -0
- package/package.json +9 -4
- package/plugin.json +1 -1
- package/src/decision-center/kernel.mjs +2 -3
- package/src/decision-center/routes.mjs +52 -26
- package/src/interop/contract/LICENSE +661 -0
- package/src/interop/contract/PROVENANCE.md +26 -0
- package/src/interop/contract/client.js +93 -0
- package/src/interop/contract/constants.js +94 -0
- package/src/interop/contract/envelope.js +83 -0
- package/src/interop/contract/index.js +20 -0
- package/src/interop/contract/negotiate.js +106 -0
- package/src/interop/contract/relay.js +229 -0
- package/src/interop/contract/server.js +77 -0
- package/src/interop/contract/transports.js +319 -0
- package/src/interop/index.mjs +270 -0
- package/src/plugin/tools.mjs +1 -1
- package/src/queue-journal/index.mjs +188 -0
- package/src/queue-journal/lib/composer-journal.mjs +128 -0
package/lib/client.js
CHANGED
|
@@ -1014,6 +1014,440 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
1014
1014
|
]);
|
|
1015
1015
|
}
|
|
1016
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* 队列日志(v3.1.0,移植自 @twinsearth/dsh-fang-queue-journal 0.1.5)。
|
|
1019
|
+
*
|
|
1020
|
+
* 草稿/队列的持久化镜像是宿主服务 `/api/w2m/composer/journal*`(见 src/queue-journal/index.mjs);
|
|
1021
|
+
* 恢复必须由用户显式点击,本模块自身从不发送消息。Fang 的独立模块入口在这里展开成同一个客户端
|
|
1022
|
+
* bundle 内的工厂函数,React 与 h 复用外层。
|
|
1023
|
+
*/
|
|
1024
|
+
function createQueueJournalModule() {
|
|
1025
|
+
|
|
1026
|
+
const DEBOUNCE_MS = 250
|
|
1027
|
+
const FALLBACK_MS = 15_000
|
|
1028
|
+
|
|
1029
|
+
/** 从 URL 里尝试取会话 id(兼容多种路由形态)。 */
|
|
1030
|
+
function sessionKeyFromUrl() {
|
|
1031
|
+
const hash = String(location.hash || '').replace(/^#/, '')
|
|
1032
|
+
const fromHash = /sessions?\/([^/?#]+)/.exec(hash)
|
|
1033
|
+
const fromPath = /sessions?\/([^/?#]+)/.exec(location.pathname)
|
|
1034
|
+
const q = new URLSearchParams(location.search).get('session') || new URLSearchParams(location.search).get('sessionId')
|
|
1035
|
+
return String(q || fromHash?.[1] || fromPath?.[1] || '').slice(0, 200)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/** 从 DOM 上找宿主暴露的会话 id(DSH 可能在容器上打 data 属性)。 */
|
|
1039
|
+
function sessionKeyFromDom() {
|
|
1040
|
+
const selectors = ['[data-session-id]', '[data-sessionid]', '[data-current-session]', '[data-active-session]']
|
|
1041
|
+
for (const selector of selectors) {
|
|
1042
|
+
const el = document.querySelector(selector)
|
|
1043
|
+
const _value = el?.getAttribute(selector.replace(/[[\]]/g, '').split('=')[0]) || ''
|
|
1044
|
+
const attr = selector.slice(1, -1)
|
|
1045
|
+
const v = el?.getAttribute(attr)
|
|
1046
|
+
if (v) return String(v).slice(0, 200)
|
|
1047
|
+
}
|
|
1048
|
+
return ''
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** 本页实例键:sessionStorage 里稳定存在,避免多会话挤在同一个 key。 */
|
|
1052
|
+
function instanceKey() {
|
|
1053
|
+
try {
|
|
1054
|
+
const cached = sessionStorage.getItem('fang-queue-journal-instance')
|
|
1055
|
+
if (cached) return cached
|
|
1056
|
+
const generated = 'page:' + Math.random().toString(36).slice(2, 10) + ':' + Date.now().toString(36)
|
|
1057
|
+
sessionStorage.setItem('fang-queue-journal-instance', generated)
|
|
1058
|
+
return generated
|
|
1059
|
+
} catch { return 'page:unknown' }
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
let activeKey = ''
|
|
1063
|
+
let activeSessionId = ''
|
|
1064
|
+
function resolveKey() {
|
|
1065
|
+
// 优先级:slot 给的 SessionId > DOM > URL > 本页实例键(绝不使用 "/")
|
|
1066
|
+
if (activeSessionId) return activeSessionId
|
|
1067
|
+
const fromUrl = sessionKeyFromUrl()
|
|
1068
|
+
const fromDom = sessionKeyFromDom()
|
|
1069
|
+
const key = String(fromDom || fromUrl || instanceKey())
|
|
1070
|
+
activeKey = key === '/' || key === '' ? instanceKey() : key
|
|
1071
|
+
return activeKey
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
async function fangToken() { return 'w2m-same-origin' }
|
|
1075
|
+
|
|
1076
|
+
async function journalRequest(method, entry, keyOverride) {
|
|
1077
|
+
const token = await fangToken()
|
|
1078
|
+
if (!token) return null
|
|
1079
|
+
const key = keyOverride || resolveKey()
|
|
1080
|
+
const res = await fetch(method === 'POST' ? '/api/w2m/composer/journal' : '/api/w2m/composer/journal?key=' + encodeURIComponent(key), {
|
|
1081
|
+
method,
|
|
1082
|
+
headers: { 'content-type': 'application/json', 'x-fang-auth': token },
|
|
1083
|
+
body: method === 'POST'
|
|
1084
|
+
? JSON.stringify({ key, sessionId: activeSessionId || undefined, sessionKey: key, path: location.hash || location.pathname, ...entry })
|
|
1085
|
+
: undefined,
|
|
1086
|
+
})
|
|
1087
|
+
if (!res.ok) return null
|
|
1088
|
+
try { return await res.json() } catch { return null }
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function journalHistory(limit = 10) {
|
|
1092
|
+
const token = await fangToken()
|
|
1093
|
+
if (!token) return []
|
|
1094
|
+
try {
|
|
1095
|
+
const res = await fetch('/api/w2m/composer/journal/history?limit=' + Number(limit) + '&sessionKey=' + encodeURIComponent(resolveKey()), {
|
|
1096
|
+
headers: { accept: 'application/json', 'x-fang-auth': token },
|
|
1097
|
+
})
|
|
1098
|
+
if (!res.ok) return []
|
|
1099
|
+
const body = await res.json()
|
|
1100
|
+
return Array.isArray(body?.items) ? body.items : []
|
|
1101
|
+
} catch { return [] }
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
async function queueRestore(sessionId, items) {
|
|
1105
|
+
const token = await fangToken()
|
|
1106
|
+
if (!token) return { ok: false, error: 'auth-unavailable' }
|
|
1107
|
+
const res = await fetch('/api/w2m/composer/queue', {
|
|
1108
|
+
method: 'POST',
|
|
1109
|
+
headers: { 'content-type': 'application/json', 'x-fang-auth': token },
|
|
1110
|
+
body: JSON.stringify({
|
|
1111
|
+
sessionId,
|
|
1112
|
+
items: items.slice(0, 100).map((item) => ({ text: String(item?.text ?? '').slice(0, 4000) })),
|
|
1113
|
+
}),
|
|
1114
|
+
})
|
|
1115
|
+
let body = null
|
|
1116
|
+
try { body = await res.json() } catch { /* non-json */ }
|
|
1117
|
+
if (!res.ok) return { ok: false, error: body?.error || 'http-' + res.status, detail: body?.detail || '' }
|
|
1118
|
+
return { ok: true, queued: Number(body?.queued) || 0, failed: Number(body?.failed) || 0 }
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function findComposer() {
|
|
1122
|
+
const nodes = [...document.querySelectorAll('textarea, [contenteditable="true"]')]
|
|
1123
|
+
.filter((el) => {
|
|
1124
|
+
if (el.closest?.('[data-fang-entry]')) return false
|
|
1125
|
+
const r = el.getBoundingClientRect()
|
|
1126
|
+
return r.width > 80 && r.height > 16
|
|
1127
|
+
})
|
|
1128
|
+
return nodes.sort((a, b) => {
|
|
1129
|
+
const ra = a.getBoundingClientRect()
|
|
1130
|
+
const rb = b.getBoundingClientRect()
|
|
1131
|
+
return (rb.width * rb.height) - (ra.width * ra.height)
|
|
1132
|
+
})[0] || null
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function readComposer(el) {
|
|
1136
|
+
if (!el) return ''
|
|
1137
|
+
return el.tagName === 'TEXTAREA' ? String(el.value || '') : String(el.innerText || '')
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function writeComposer(text) {
|
|
1141
|
+
const el = findComposer()
|
|
1142
|
+
if (!el) return false
|
|
1143
|
+
el.focus()
|
|
1144
|
+
if (el.tagName === 'TEXTAREA') {
|
|
1145
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set
|
|
1146
|
+
if (setter) setter.call(el, text)
|
|
1147
|
+
else el.value = text
|
|
1148
|
+
el.dispatchEvent(new Event('input', { bubbles: true }))
|
|
1149
|
+
} else {
|
|
1150
|
+
try {
|
|
1151
|
+
document.execCommand('selectAll', false, null)
|
|
1152
|
+
document.execCommand('insertText', false, text)
|
|
1153
|
+
} catch { /* fall back */ }
|
|
1154
|
+
if (!String(el.innerText || '').includes(text.slice(0, 20))) {
|
|
1155
|
+
el.textContent = text
|
|
1156
|
+
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }))
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
el.focus()
|
|
1160
|
+
return true
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function setDraftViaApi(props, text) {
|
|
1164
|
+
try {
|
|
1165
|
+
if (props?.inputActions && typeof props.inputActions.setDraft === 'function') {
|
|
1166
|
+
props.inputActions.setDraft(String(text || ''))
|
|
1167
|
+
return true
|
|
1168
|
+
}
|
|
1169
|
+
} catch { /* fall back to the DOM */ }
|
|
1170
|
+
return writeComposer(String(text || ''))
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const QUEUE_COUNT_RE = /(\d+)\s*(条排队消息|queued messages|件のキュー済みメッセージ|mensagens na fila|messages en file d'attente|nachrichten in der warteschlange|сообщения в очереди|mensajes en cola|mensagens na fila)/i
|
|
1174
|
+
const QUEUE_ACTION_LABELS = /^(插话发送|编辑排队消息|删除排队消息|保存排队消息|取消编辑|发送中…|排队消息图片|queued message image|edit queued message|remove queued message|save queued message|cancel edit|sending…|steer)$/i
|
|
1175
|
+
|
|
1176
|
+
/** 从 QueueDock 行里去掉操作按钮,只留消息正文。 */
|
|
1177
|
+
function queueRowText(row) {
|
|
1178
|
+
try {
|
|
1179
|
+
const clone = row.cloneNode(true)
|
|
1180
|
+
for (const node of clone.querySelectorAll('button, [role="button"]')) node.remove()
|
|
1181
|
+
const lines = String(clone.innerText || clone.textContent || '')
|
|
1182
|
+
.split(/\r?\n/)
|
|
1183
|
+
.map((line) => line.trim())
|
|
1184
|
+
.filter((line) => line && !QUEUE_ACTION_LABELS.test(line))
|
|
1185
|
+
return lines.join(' ').trim()
|
|
1186
|
+
} catch { return '' }
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* DSH QueueDock 的 CSS 是 hashed class(F07MqG_*),不能靠 class*=queue。
|
|
1191
|
+
* 以 header 的“N 条排队消息”文案定位,再通过 aria-controls 找到列表。
|
|
1192
|
+
*/
|
|
1193
|
+
function scanQueueDock() {
|
|
1194
|
+
const out = []
|
|
1195
|
+
const seen = new Set()
|
|
1196
|
+
const pushText = (text) => {
|
|
1197
|
+
const value = String(text || '').trim()
|
|
1198
|
+
if (!value || value.length > 4000 || seen.has(value)) return
|
|
1199
|
+
seen.add(value)
|
|
1200
|
+
out.push({ text: value, at: Date.now() })
|
|
1201
|
+
}
|
|
1202
|
+
const docks = document.querySelectorAll('[data-queue-dock]')
|
|
1203
|
+
for (const dock of docks) {
|
|
1204
|
+
for (const row of dock.querySelectorAll('li')) pushText(queueRowText(row))
|
|
1205
|
+
}
|
|
1206
|
+
if (docks.length > 0) return out
|
|
1207
|
+
|
|
1208
|
+
const headers = [...document.querySelectorAll('button[aria-controls], [role="button"][aria-controls]')]
|
|
1209
|
+
.filter((el) => QUEUE_COUNT_RE.test(String(el.textContent || '')))
|
|
1210
|
+
for (const header of headers) {
|
|
1211
|
+
const listId = header.getAttribute('aria-controls')
|
|
1212
|
+
const list = (listId ? document.getElementById(listId) : null) || header.parentElement?.querySelector('ul')
|
|
1213
|
+
if (!list) continue
|
|
1214
|
+
for (const row of list.querySelectorAll('li')) pushText(queueRowText(row))
|
|
1215
|
+
}
|
|
1216
|
+
return out
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function scanQueue() {
|
|
1220
|
+
const found = scanQueueDock()
|
|
1221
|
+
if (found.length > 0) return found.slice(-100)
|
|
1222
|
+
const seen = new Set()
|
|
1223
|
+
const out = []
|
|
1224
|
+
const nodes = document.querySelectorAll('[class*="queue" i], [data-testid*="queue" i], [aria-label*="queue" i], [class*="pending" i], [data-testid*="pending" i], [aria-label*="排队"], [title*="排队"]')
|
|
1225
|
+
for (const el of nodes) {
|
|
1226
|
+
const text = String(el.innerText || '').trim()
|
|
1227
|
+
if (!text || text.length > 4000 || seen.has(text)) continue
|
|
1228
|
+
seen.add(text)
|
|
1229
|
+
out.push({ text, at: Date.now() })
|
|
1230
|
+
}
|
|
1231
|
+
return out.slice(-100)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/** 回读校验:确认服务端存下来的队列文本与本地一致(并检测 "????" 乱码)。 */
|
|
1235
|
+
async function verifySaved(expectedQueue) {
|
|
1236
|
+
const body = await journalRequest('GET')
|
|
1237
|
+
const saved = Array.isArray(body?.entry?.queue) ? body.entry.queue.map((item) => String(item?.text || '')) : []
|
|
1238
|
+
const expected = expectedQueue.map((item) => String(item?.text || ''))
|
|
1239
|
+
const sameLength = saved.length === expected.length
|
|
1240
|
+
const sameText = sameLength && saved.every((text, index) => text === expected[index])
|
|
1241
|
+
const looksMojibake = saved.some((text) => /\?{3,}/.test(text) && !/\?{3,}/.test(expected[saved.indexOf(text)] || ''))
|
|
1242
|
+
return { ok: sameText && !looksMojibake, sameLength, sameText, looksMojibake, saved }
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function JournalDock(props) {
|
|
1246
|
+
const sessionId = String(props?.sessionId || '')
|
|
1247
|
+
const [entry, setEntry] = React.useState(null)
|
|
1248
|
+
const [history, setHistory] = React.useState([])
|
|
1249
|
+
const [status, setStatus] = React.useState('')
|
|
1250
|
+
const [showQueue, setShowQueue] = React.useState(false)
|
|
1251
|
+
const [showHistory, setShowHistory] = React.useState(false)
|
|
1252
|
+
|
|
1253
|
+
React.useEffect(() => {
|
|
1254
|
+
activeSessionId = sessionId
|
|
1255
|
+
setEntry(null)
|
|
1256
|
+
setShowQueue(false)
|
|
1257
|
+
setShowHistory(false)
|
|
1258
|
+
let alive = true
|
|
1259
|
+
journalRequest('GET').then(async (body) => {
|
|
1260
|
+
if (alive) setEntry(body?.entry ?? null)
|
|
1261
|
+
}).catch(() => {})
|
|
1262
|
+
journalHistory(10).then((items) => { if (alive) setHistory(items) }).catch(() => {})
|
|
1263
|
+
return () => { alive = false }
|
|
1264
|
+
}, [sessionId])
|
|
1265
|
+
|
|
1266
|
+
const queue = Array.isArray(entry?.queue) ? entry.queue : []
|
|
1267
|
+
const hasEntry = Boolean(entry && (entry.draft || queue.length))
|
|
1268
|
+
|
|
1269
|
+
if (!hasEntry && history.length === 0) return null
|
|
1270
|
+
|
|
1271
|
+
const restoreItems = async (items, draft) => {
|
|
1272
|
+
const draftText = String(draft || '')
|
|
1273
|
+
const draftOk = draftText ? setDraftViaApi(props, draftText) : true
|
|
1274
|
+
if (!draftOk) { setStatus('未找到输入框,请先打开会话'); return }
|
|
1275
|
+
if (items.length === 0) {
|
|
1276
|
+
await journalRequest('POST', { draft: '', queue: [] })
|
|
1277
|
+
setEntry(null)
|
|
1278
|
+
setStatus('草稿已放入官方输入框')
|
|
1279
|
+
return
|
|
1280
|
+
}
|
|
1281
|
+
if (!sessionId) { setStatus('草稿已恢复;队列需要先选中会话'); return }
|
|
1282
|
+
const restored = await queueRestore(sessionId, items)
|
|
1283
|
+
if (!restored.ok) {
|
|
1284
|
+
setStatus(restored.error === 'session-not-live'
|
|
1285
|
+
? '草稿已恢复;会话未运行,队列保留待重试'
|
|
1286
|
+
: '草稿已恢复;队列恢复失败:' + restored.error)
|
|
1287
|
+
return
|
|
1288
|
+
}
|
|
1289
|
+
await journalRequest('POST', { draft: '', queue: [] })
|
|
1290
|
+
setEntry(null)
|
|
1291
|
+
setStatus(`已恢复 ${restored.queued} 条排队消息${restored.failed ? `(${restored.failed} 条失败)` : ''}`)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const rowText = (item) => {
|
|
1295
|
+
const text = String(item?.text || '').replace(/\s+/g, ' ').trim()
|
|
1296
|
+
return text.length > 140 ? text.slice(0, 137) + '…' : text
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const snapshotLabel = (snap) => {
|
|
1300
|
+
const at = String(snap?.updatedAt || '')
|
|
1301
|
+
const count = Array.isArray(snap?.queue) ? snap.queue.length : 0
|
|
1302
|
+
return `${at ? at.replace('T', ' ').slice(0, 16) : '?'} · ${count} 条`
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
return h('div', {
|
|
1306
|
+
'data-fang-entry': 'queue-journal',
|
|
1307
|
+
style: {
|
|
1308
|
+
display: 'inline-flex', flexDirection: 'column', gap: 6, margin: '2px 0', maxWidth: '100%',
|
|
1309
|
+
padding: '3px 6px', border: '1px solid rgba(127,127,127,.3)', borderRadius: 9,
|
|
1310
|
+
fontSize: 12, background: 'rgba(127,127,127,.06)',
|
|
1311
|
+
},
|
|
1312
|
+
},
|
|
1313
|
+
h('div', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } },
|
|
1314
|
+
hasEntry ? h('button', {
|
|
1315
|
+
type: 'button',
|
|
1316
|
+
title: `恢复草稿${queue.length ? `,并把这 ${queue.length} 条排队消息放回官方输入队列` : ''}`,
|
|
1317
|
+
'aria-label': '恢复全部草稿与队列',
|
|
1318
|
+
onClick: () => { void restoreItems(queue, entry?.draft) },
|
|
1319
|
+
style: { height: 26, padding: '0 8px', borderRadius: 7, cursor: 'pointer' },
|
|
1320
|
+
}, queue.length ? `恢复全部 (${queue.length + (entry?.draft ? 1 : 0)})` : '恢复草稿') : null,
|
|
1321
|
+
hasEntry && queue.length > 0 ? h('button', {
|
|
1322
|
+
type: 'button', title: '展开保存的排队消息列表', 'aria-expanded': showQueue ? 'true' : 'false',
|
|
1323
|
+
onClick: () => setShowQueue((value) => !value),
|
|
1324
|
+
style: { height: 26, padding: '0 8px', borderRadius: 7, cursor: 'pointer' },
|
|
1325
|
+
}, `队列 ${queue.length}`) : null,
|
|
1326
|
+
history.length > 0 ? h('button', {
|
|
1327
|
+
type: 'button', title: '从历史快照中挑选一份恢复(主记录被覆盖时的兜底)',
|
|
1328
|
+
'aria-expanded': showHistory ? 'true' : 'false',
|
|
1329
|
+
onClick: () => setShowHistory((value) => !value),
|
|
1330
|
+
style: { height: 26, padding: '0 8px', borderRadius: 7, cursor: 'pointer' },
|
|
1331
|
+
}, `历史 ${history.length}`) : null,
|
|
1332
|
+
hasEntry ? h('button', {
|
|
1333
|
+
type: 'button', title: '清除本地保存的草稿与排队快照', 'aria-label': '清除草稿',
|
|
1334
|
+
onClick: async () => { await journalRequest('POST', { draft: '', queue: [] }); setEntry(null); setStatus('已清除记录') },
|
|
1335
|
+
style: { height: 26, padding: '0 8px', borderRadius: 7, cursor: 'pointer' },
|
|
1336
|
+
}, '清除') : null,
|
|
1337
|
+
),
|
|
1338
|
+
showQueue && queue.length > 0 ? h('div', {
|
|
1339
|
+
role: 'list', style: { display: 'grid', gap: 4, maxHeight: 180, overflow: 'auto', padding: '2px 0' },
|
|
1340
|
+
}, ...queue.map((item, index) => h('div', {
|
|
1341
|
+
role: 'listitem', key: `${index}:${item?.at || ''}`, title: String(item?.text || ''),
|
|
1342
|
+
style: { padding: '3px 6px', border: '1px solid rgba(127,127,127,.22)', borderRadius: 6, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
|
|
1343
|
+
}, `${index + 1}. ${rowText(item)}`))) : null,
|
|
1344
|
+
showHistory && history.length > 0 ? h('div', {
|
|
1345
|
+
role: 'list', style: { display: 'grid', gap: 4, maxHeight: 200, overflow: 'auto', padding: '2px 0' },
|
|
1346
|
+
}, ...history.slice(0, 10).map((snap, index) => h('div', {
|
|
1347
|
+
role: 'listitem', key: `h${index}:${snap?.updatedAt || ''}`,
|
|
1348
|
+
style: { display: 'flex', alignItems: 'center', gap: 6, padding: '3px 6px', border: '1px solid rgba(127,127,127,.22)', borderRadius: 6 },
|
|
1349
|
+
}, h('span', { style: { flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, snapshotLabel(snap)),
|
|
1350
|
+
h('button', {
|
|
1351
|
+
type: 'button', title: '把这份快照里的队列放回官方队列',
|
|
1352
|
+
onClick: async () => { await restoreItems(Array.isArray(snap?.queue) ? snap.queue : [], '') },
|
|
1353
|
+
style: { height: 22, padding: '0 6px', borderRadius: 6, cursor: 'pointer' },
|
|
1354
|
+
}, '恢复')))) : null,
|
|
1355
|
+
h('span', {
|
|
1356
|
+
role: 'status', 'aria-live': 'polite',
|
|
1357
|
+
style: status ? { padding: '1px 2px', opacity: .8 } : { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)', whiteSpace: 'nowrap' },
|
|
1358
|
+
}, status))
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
let saveTimer = null
|
|
1362
|
+
let lastSignature = ''
|
|
1363
|
+
let flushTimer = null
|
|
1364
|
+
|
|
1365
|
+
function commitSave() {
|
|
1366
|
+
const text = readComposer(findComposer())
|
|
1367
|
+
const queue = scanQueue()
|
|
1368
|
+
const signature = JSON.stringify({ key: resolveKey(), text, queue: queue.map((item) => item.text) })
|
|
1369
|
+
if (signature === lastSignature) return
|
|
1370
|
+
lastSignature = signature
|
|
1371
|
+
void (async () => {
|
|
1372
|
+
const saved = await journalRequest('POST', { draft: text, queue })
|
|
1373
|
+
if (!saved) return
|
|
1374
|
+
const check = await verifySaved(queue)
|
|
1375
|
+
if (!check.ok) {
|
|
1376
|
+
// 回读不一致(或被截断/乱码):重试一次,并把诊断留在控制台
|
|
1377
|
+
try { window.console?.warn?.('[fang-queue-journal] 回读校验未通过', check) } catch { /* ignore */ }
|
|
1378
|
+
const retried = await journalRequest('POST', { draft: text, queue })
|
|
1379
|
+
if (retried) await verifySaved(queue)
|
|
1380
|
+
}
|
|
1381
|
+
})()
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function scheduleSave() {
|
|
1385
|
+
if (saveTimer) clearTimeout(saveTimer)
|
|
1386
|
+
saveTimer = setTimeout(() => { saveTimer = null; commitSave() }, DEBOUNCE_MS)
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/** 关闭/切页前同步 flush:fetch keepalive 保证请求活着离开页面。 */
|
|
1390
|
+
function flushNow() {
|
|
1391
|
+
const text = readComposer(findComposer())
|
|
1392
|
+
const queue = scanQueue()
|
|
1393
|
+
const signature = JSON.stringify({ key: resolveKey(), text, queue: queue.map((item) => item.text) })
|
|
1394
|
+
if (signature === lastSignature) return
|
|
1395
|
+
lastSignature = signature
|
|
1396
|
+
void (async () => {
|
|
1397
|
+
const token = await fangToken()
|
|
1398
|
+
if (!token) return
|
|
1399
|
+
try {
|
|
1400
|
+
fetch('/api/w2m/composer/journal', {
|
|
1401
|
+
method: 'POST',
|
|
1402
|
+
keepalive: true,
|
|
1403
|
+
headers: { 'content-type': 'application/json', 'x-fang-auth': token },
|
|
1404
|
+
body: JSON.stringify({ key: resolveKey(), sessionKey: resolveKey(), sessionId: activeSessionId || undefined, draft: text, queue, path: location.hash || location.pathname }),
|
|
1405
|
+
})
|
|
1406
|
+
} catch { /* ignore */ }
|
|
1407
|
+
})()
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
return {
|
|
1411
|
+
inject: ['slots'],
|
|
1412
|
+
apply(ctx) {
|
|
1413
|
+
ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
1414
|
+
name: 'conversation.composer.dock',
|
|
1415
|
+
id: 'fang-queue-journal',
|
|
1416
|
+
order: 90,
|
|
1417
|
+
}, JournalDock))
|
|
1418
|
+
|
|
1419
|
+
// ① 草稿输入:捕获阶段监听,避免被宿主 stopPropagation 吞掉
|
|
1420
|
+
document.addEventListener('input', (event) => {
|
|
1421
|
+
const target = event.target
|
|
1422
|
+
if (!target || (target.tagName !== 'TEXTAREA' && target.getAttribute?.('contenteditable') !== 'true')) return
|
|
1423
|
+
scheduleSave()
|
|
1424
|
+
}, true)
|
|
1425
|
+
|
|
1426
|
+
// ② 队列变化:只盯 QueueDock(body 全量只用 childList,避免 characterData 风暴)
|
|
1427
|
+
try {
|
|
1428
|
+
const queueObserver = new MutationObserver(() => scheduleSave())
|
|
1429
|
+
queueObserver.observe(document.body, { childList: true, subtree: true })
|
|
1430
|
+
// 有 [data-queue-dock] 时再额外盯 characterData(行文本可能原地更新)
|
|
1431
|
+
const docksObserver = new MutationObserver(() => scheduleSave())
|
|
1432
|
+
for (const dock of document.querySelectorAll('[data-queue-dock]')) {
|
|
1433
|
+
docksObserver.observe(dock, { childList: true, subtree: true, characterData: true })
|
|
1434
|
+
}
|
|
1435
|
+
queueObserver.__docksObserver = docksObserver
|
|
1436
|
+
} catch { /* DOM unavailable */ }
|
|
1437
|
+
|
|
1438
|
+
// ③ 15s 兜底(覆盖“宿主重渲染后新 dock 未被观察到”的情况)
|
|
1439
|
+
flushTimer = setInterval(() => scheduleSave(), FALLBACK_MS)
|
|
1440
|
+
try { flushTimer?.unref?.() } catch { /* browser timer */ }
|
|
1441
|
+
|
|
1442
|
+
// ④ 关闭/切页前 flush
|
|
1443
|
+
const onPageHide = () => flushNow()
|
|
1444
|
+
const onVisibility = () => { if (document.visibilityState === 'hidden') flushNow() }
|
|
1445
|
+
window.addEventListener('pagehide', onPageHide)
|
|
1446
|
+
document.addEventListener('visibilitychange', onVisibility)
|
|
1447
|
+
window.addEventListener('beforeunload', onPageHide)
|
|
1448
|
+
},
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1017
1451
|
/** v1.9.4: the left sidebar entry: globe + GPW, between Plugins and Automation. */
|
|
1018
1452
|
function GpwSidebarIcon() {
|
|
1019
1453
|
return h('button', {
|
|
@@ -3511,6 +3945,16 @@ const CHAT_EMOJIS = CHAT_COMMANDS;
|
|
|
3511
3945
|
|
|
3512
3946
|
|
|
3513
3947
|
|
|
3948
|
+
// v3.1.0: 队列日志(移植自 @twinsearth/dsh-fang-queue-journal):把草稿与队列镜像存到
|
|
3949
|
+
// /api/w2m/composer/journal*,恢复只由用户点击触发。
|
|
3950
|
+
try {
|
|
3951
|
+
if (typeof document !== 'undefined' && ctx !== null && typeof ctx === 'object' && ctx.slots !== undefined) {
|
|
3952
|
+
createQueueJournalModule().apply({ slots: ctx.slots });
|
|
3953
|
+
}
|
|
3954
|
+
} catch (error) {
|
|
3955
|
+
console.error('@twinsearth/w2m-dsh-plugin: the queue journal was not mounted; the rest of the page is unaffected', error);
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3514
3958
|
// v1.9.9: in-conversation search, in the session header next to Pin.
|
|
3515
3959
|
try {
|
|
3516
3960
|
slots.inject('conversation.session.header.actions', () => slots.register({
|
package/lib/contract.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-facing entry for W2M's interop contract layer: `@twinsearth/w2m-dsh-plugin/contract`.
|
|
3
|
+
*
|
|
4
|
+
* The documented subpath a profile row names to expose `w2m.*` methods (local service + HTTP routes)
|
|
5
|
+
* and to reach peers through the published contract SDK's local → http → file → legacy fallback.
|
|
6
|
+
* The vendored SDK copy and its provenance live in `src/interop/contract/`.
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
CONTRACT_LOCAL_SERVICE,
|
|
10
|
+
CONTRACT_ROUTE_PREFIX,
|
|
11
|
+
W2M_CONTRACT_CAPABILITIES,
|
|
12
|
+
W2M_CONTRACT_SERVICE,
|
|
13
|
+
W2M_CONTRACT_VERSION,
|
|
14
|
+
W2M_METHODS,
|
|
15
|
+
W2M_METHOD_VERSION,
|
|
16
|
+
apply,
|
|
17
|
+
createW2mContractClient,
|
|
18
|
+
createW2mContractServer,
|
|
19
|
+
inject,
|
|
20
|
+
name,
|
|
21
|
+
registerContractRoutes,
|
|
22
|
+
} from '../src/interop/index.mjs';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-facing entry for W2M's queue journal: `@twinsearth/w2m-dsh-plugin/queue-journal`.
|
|
3
|
+
*
|
|
4
|
+
* The documented subpath a profile row names to enable draft/queue persistence. The host logic is
|
|
5
|
+
* `src/queue-journal/index.mjs`; the composer UI ships in the package's one client bundle
|
|
6
|
+
* (`lib/client.js`), the same shape GPW and the decision center use.
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
apply,
|
|
10
|
+
inject,
|
|
11
|
+
name,
|
|
12
|
+
QUEUE_JOURNAL_ROUTE_PREFIX,
|
|
13
|
+
QUEUE_JOURNAL_SERVICE,
|
|
14
|
+
QUEUE_RESTORE_ROUTE_PATH,
|
|
15
|
+
} from '../src/queue-journal/index.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"./desktop": "./lib/desktop.js",
|
|
16
16
|
"./decision-center": "./lib/decision-center.js",
|
|
17
17
|
"./locale/en.json": "./locale/en.json",
|
|
18
|
-
"./locale/zh.json": "./locale/zh.json"
|
|
18
|
+
"./locale/zh.json": "./locale/zh.json",
|
|
19
|
+
"./queue-journal": "./lib/queue-journal.js",
|
|
20
|
+
"./contract": "./lib/contract.js"
|
|
19
21
|
},
|
|
20
22
|
"dsh": {
|
|
21
23
|
"plugin": true,
|
|
@@ -73,7 +75,7 @@
|
|
|
73
75
|
"pack": "node scripts/pack.mjs",
|
|
74
76
|
"reachability": "node scripts/reachability-check.mjs",
|
|
75
77
|
"reachability:classify": "node scripts/reachability-classify.mjs",
|
|
76
|
-
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.test.mjs test/gpw-modes.test.mjs test/joint-cli.test.mjs test/joint-command-hash.test.mjs test/joint-dispatch-audit.test.mjs test/tools-cancel-auth.test.mjs test/fleet-mirror.test.mjs test/sink-git-ssh.test.mjs test/agent-sse-watchdog.test.mjs test/version-gate.test.mjs test/joint-merge-cli.test.mjs test/joint-progress.test.mjs test/project-room-truncation.test.mjs test/chat-crypto-aad.test.mjs test/agent-artifact-delivery.test.mjs test/dataplane-sink-route.test.mjs test/artifact-sink-e2e.test.mjs test/project-room-persistence.test.mjs test/project-room-disk-persistence.test.mjs test/project-stream-route.test.mjs test/chat-v2-gate.test.mjs test/xiangqi.test.mjs test/xfer-foundation.test.mjs test/xfer-ferry.test.mjs test/xfer-publish.test.mjs test/xfer-want-block.test.mjs test/p2p-cipher-diag.test.mjs test/chacha-fallback.test.mjs test/fleet-binding.test.mjs test/fleet-home.test.mjs test/fleet-config.test.mjs test/fleet-config-route.test.mjs test/xfer-channel.test.mjs test/p2p-race.test.mjs test/path-scheduler.test.mjs test/transport.test.mjs test/iroh-sidecar.test.mjs test/discovery.test.mjs test/p2p-rtt.test.mjs test/artifact-ref-v3.test.mjs test/xfer-recovery.test.mjs test/xfer-router.test.mjs test/remote-s2-capture.test.mjs test/view-capture.test.mjs test/view-frame-channel.test.mjs test/view-consent.test.mjs test/remote-view-route.test.mjs test/remote-media.test.mjs test/view-session-key.test.mjs test/remote-requests.test.mjs test/relay-target.test.mjs test/decision-center.test.mjs test/decision-routes.test.mjs test/decision-outbox.test.mjs",
|
|
78
|
+
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs test/task-deps.test.mjs test/dataflow-wiring.test.mjs test/task-graph.test.mjs test/ask-store.test.mjs test/turn.test.mjs test/fleet-members.test.mjs test/redact.test.mjs test/policy-matrix.test.mjs test/envelope-signature.test.mjs test/chat-crypto.test.mjs test/reduce-suggestion.test.mjs test/merge-hint.test.mjs test/fleet-chat-route.test.mjs test/maintenance.test.mjs test/maintenance-ferry.test.mjs test/grant.test.mjs test/grant-gate.test.mjs test/worktree.test.mjs test/patch-artifact.test.mjs test/merge-queue.test.mjs test/m3-workflow.test.mjs test/project-room.test.mjs test/project-attribution.test.mjs test/project-locks.test.mjs test/project-reviews.test.mjs test/project-notifications.test.mjs test/blob-sessions.test.mjs test/outbox.test.mjs test/reliability-metrics.test.mjs test/evidence.test.mjs test/grant-issue.test.mjs test/project-refs.test.mjs test/exec-capability.test.mjs test/secondary-link.test.mjs test/bootstrap-cli.test.mjs test/bootstrap.test.mjs test/windows-hidden-task.test.mjs test/relay-failover.test.mjs test/npm-update.test.mjs test/agent-start-nonblocking.test.mjs test/fleet-chat-capture.test.mjs test/capture-route.test.mjs test/update-resolver.test.mjs test/agent-identity-safe.test.mjs test/re-pair-stale-token.test.mjs test/gpw-folder-policy.test.mjs test/gpw-activation.test.mjs test/gpw-hardening.test.mjs test/gpw-tamper-gate.test.mjs test/gpw-persistence.test.mjs test/gpw-colors.test.mjs test/gpw-recent-fold.test.mjs test/gpw-auto-approve.test.mjs test/joint-dev-dispatch.test.mjs test/joint-dev-loop.test.mjs test/joint-dev-audit.test.mjs test/joint-dev-redline.test.mjs test/relay-redline-guard.test.mjs test/relay-p0-projection.test.mjs test/guarded-dispatch.test.mjs test/p0-single-matcher.test.mjs test/path-policy.test.mjs test/path-policy-fs.test.mjs test/gpw-inheritance-table.test.mjs test/gpw-policy-adapter.test.mjs test/target-policy.test.mjs test/relay-redline-audit.test.mjs test/relay-repair-ledger.test.mjs test/relay-purge-blobs.test.mjs test/relay-scrub.test.mjs test/joint-dev-plan-store.test.mjs test/relay-basepath-blobs.test.mjs test/operator-token.test.mjs test/relay-health-capabilities.test.mjs test/joint-relay-dispatch.test.mjs test/operator-token-route.test.mjs test/relay-scrub-task-projection.test.mjs test/operator-key-approval.test.mjs test/relay-offer-journal.test.mjs test/joint-r5-wiring.test.mjs test/p1-hardening.test.mjs test/joint-merge-engine.test.mjs test/artifact-ref-dir-sink.test.mjs test/chat-sticker-guard.test.mjs test/relay-task-progress.test.mjs test/gpw-modes.test.mjs test/joint-cli.test.mjs test/joint-command-hash.test.mjs test/joint-dispatch-audit.test.mjs test/tools-cancel-auth.test.mjs test/fleet-mirror.test.mjs test/sink-git-ssh.test.mjs test/agent-sse-watchdog.test.mjs test/version-gate.test.mjs test/joint-merge-cli.test.mjs test/joint-progress.test.mjs test/project-room-truncation.test.mjs test/chat-crypto-aad.test.mjs test/agent-artifact-delivery.test.mjs test/dataplane-sink-route.test.mjs test/artifact-sink-e2e.test.mjs test/project-room-persistence.test.mjs test/project-room-disk-persistence.test.mjs test/project-stream-route.test.mjs test/chat-v2-gate.test.mjs test/xiangqi.test.mjs test/xfer-foundation.test.mjs test/xfer-ferry.test.mjs test/xfer-publish.test.mjs test/xfer-want-block.test.mjs test/p2p-cipher-diag.test.mjs test/chacha-fallback.test.mjs test/fleet-binding.test.mjs test/fleet-home.test.mjs test/fleet-config.test.mjs test/fleet-config-route.test.mjs test/xfer-channel.test.mjs test/p2p-race.test.mjs test/path-scheduler.test.mjs test/transport.test.mjs test/iroh-sidecar.test.mjs test/discovery.test.mjs test/p2p-rtt.test.mjs test/artifact-ref-v3.test.mjs test/xfer-recovery.test.mjs test/xfer-router.test.mjs test/remote-s2-capture.test.mjs test/view-capture.test.mjs test/view-frame-channel.test.mjs test/view-consent.test.mjs test/remote-view-route.test.mjs test/remote-media.test.mjs test/view-session-key.test.mjs test/remote-requests.test.mjs test/relay-target.test.mjs test/decision-center.test.mjs test/decision-routes.test.mjs test/decision-outbox.test.mjs test/queue-journal.test.mjs test/contract-interop.test.mjs",
|
|
77
79
|
"test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
|
|
78
80
|
"test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
|
|
79
81
|
"test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
|
|
@@ -173,7 +175,10 @@
|
|
|
173
175
|
"test:project-refs": "node --test --test-force-exit test/project-refs.test.mjs",
|
|
174
176
|
"test:secondary-link": "node --test --test-force-exit test/secondary-link.test.mjs",
|
|
175
177
|
"verify:fleet-isolation": "node scripts/verify-fleet-isolation.mjs",
|
|
176
|
-
"test:decision-center": "node --test --test-force-exit test/decision-center.test.mjs test/decision-routes.test.mjs test/decision-outbox.test.mjs"
|
|
178
|
+
"test:decision-center": "node --test --test-force-exit test/decision-center.test.mjs test/decision-routes.test.mjs test/decision-outbox.test.mjs",
|
|
179
|
+
"test:queue-journal": "node --test --test-force-exit test/queue-journal.test.mjs",
|
|
180
|
+
"test:contract": "node --test --test-force-exit test/contract-interop.test.mjs",
|
|
181
|
+
"verify:contract": "node scripts/check-contract-vendor.mjs"
|
|
177
182
|
},
|
|
178
183
|
"devDependencies": {
|
|
179
184
|
"@eslint/js": "^10.0.1",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "twinsearth-w2m-dsh",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.2.0",
|
|
5
5
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -147,9 +147,8 @@ export function writeJsonAtomic(file, value, { indent = 2 } = {}) {
|
|
|
147
147
|
export function resolveFangLayout(options = {}) {
|
|
148
148
|
const { dataRoot = '' } = options;
|
|
149
149
|
const explicit = String(dataRoot ?? '').trim();
|
|
150
|
-
const dataDir =
|
|
151
|
-
? path.resolve(explicit)
|
|
152
|
-
: path.join(resolveStateDir(null, ''), 'decision-center');
|
|
150
|
+
const dataDir =
|
|
151
|
+
explicit !== '' ? path.resolve(explicit) : path.join(resolveStateDir(null, ''), 'decision-center');
|
|
153
152
|
return { dataDir };
|
|
154
153
|
}
|
|
155
154
|
|