@ljwei-stak/model-router-galgame 0.4.12 → 0.4.15

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.
@@ -61740,7 +61740,7 @@ var gal_scene_default = { version: 1, settings: { stageW: 1920, stageH: 1080, sh
61740
61740
  var name = "gal-view";
61741
61741
  var PROJECT_URL = "https://github.com/ljwei-stak/deepseek-harness";
61742
61742
  var RELEASES_URL = `${PROJECT_URL}/releases`;
61743
- var PLUGIN_VERSION = "0.4.12";
61743
+ var PLUGIN_VERSION = "0.4.14";
61744
61744
  function createUpdateApi() {
61745
61745
  const bridge = globalThis.deepSeekHarnessDesktop;
61746
61746
  const openExternal = (url) => {
@@ -15,6 +15,17 @@ import {
15
15
  modLensUpstream,
16
16
  routeThroughModLens,
17
17
  } from './shared/modlens-routing.mjs'
18
+ import {
19
+ approvalGateStatus,
20
+ approvalSafetyContext,
21
+ decorateApprovalReason,
22
+ isApprovalGateReason,
23
+ } from './shared/approval-gate.mjs'
24
+ import {
25
+ webCapabilityForPlan,
26
+ webCapabilityStatus,
27
+ webInstruction,
28
+ } from './shared/web-routing.mjs'
18
29
 
19
30
  let settingsRuntimePromise
20
31
  let routerSettings = { ...DEFAULT_ROUTER_SETTINGS }
@@ -213,6 +224,17 @@ function stageMessage(plan, step) {
213
224
  }
214
225
  }
215
226
 
227
+ function webMessage(plan) {
228
+ const text = webInstruction(plan?.web)
229
+ if (text === '') return null
230
+ return {
231
+ id: newMessageId(),
232
+ role: 'user',
233
+ content: [{ type: 'text', text }],
234
+ source: { kind: 'plugin', plugin: name, form: 'web-capability', summary: '联网与可见浏览器策略' },
235
+ }
236
+ }
237
+
216
238
  /**
217
239
  * Persona is a final-answer context only. It is intentionally a separate
218
240
  * message so the collaboration stages and their audit records remain free of
@@ -268,6 +290,7 @@ function analysisMessage(plan) {
268
290
  `缓存计费比例:读取 ${Math.round(Number(plan.optimization?.cacheReadRatio ?? 0) * 100)}%,写入 ${Math.round(Number(plan.optimization?.cacheWriteRatio ?? 0) * 100)}%(未填写时按普通输入计费)`,
269
291
  Number(plan.optimization?.budgetUsd ?? 0) > 0 ? `预算上限:$${Number(plan.optimization.budgetUsd).toFixed(6)};${plan.optimization.budgetExceeded ? '仍超预算,已在质量下限内尽量压缩' : '满足预算约束'}` : '',
270
292
  `LiveBench:${plan.optimization?.liveBench?.fetchedAt ? `快照于 ${new Date(Number(plan.optimization.liveBench.fetchedAt)).toISOString()}${plan.optimization.liveBench.stale ? '(本次刷新失败,沿用上次快照)' : ''}` : '未完成联网核验,使用实验基线'}`,
293
+ plan.web?.needsWeb ? `联网策略:${plan.web.directBrowser ? 'Ego Browser 可见窗口优先' : 'ModSearch 搜索/抓取,失败时 Ego Browser 窗口兜底'};反爬处理:人工接管后继续` : '',
271
294
  String(plan.reason ?? ''),
272
295
  ].filter(Boolean).join('\n')
273
296
  return {
@@ -432,6 +455,27 @@ export function apply(ctx) {
432
455
  routerSettingsPromise = registerRouterSettings(ctx)
433
456
  }
434
457
  const scheduleOpenCodeRepair = createOpenCodeRepairScheduler(ctx)
458
+
459
+ // dsh-approval-gate owns the actual decision. The router adds auditable
460
+ // stage/route context before that waterfall so multi-task escalations are
461
+ // visible to the gate's Flash classifier and human reviewer. The request
462
+ // object is borrowed by the Host approval service for this dispatch only.
463
+ ctx.on('approval/request', (request, next) => {
464
+ if (!isApprovalGateReason(request?.reason)) return next()
465
+ const state = request?.agent === undefined ? null : stateFor(request.agent)
466
+ const context = approvalSafetyContext(state, state?.lastStep)
467
+ const decorated = decorateApprovalReason(request.reason, context)
468
+ if (decorated !== request.reason) {
469
+ try {
470
+ request.reason = decorated
471
+ } catch {
472
+ // Some hosts freeze event payloads. In that case the gate still
473
+ // receives the original reason and remains fully fail-safe.
474
+ }
475
+ }
476
+ return next()
477
+ }, { prepend: true })
478
+
435
479
  ctx.commands.register({
436
480
  name: 'router',
437
481
  description: 'switch Model Router mode or inspect the latest routing plan',
@@ -440,7 +484,7 @@ export function apply(ctx) {
440
484
  // the whole composer submission; the GAL client sends this command
441
485
  // without image bytes so the attachment remains available for the next
442
486
  // user turn.
443
- input: { hint: 'mode collective|single | plan', images: true },
487
+ input: { hint: 'mode collective|single | plan | safety', images: true },
444
488
  recordInput: true,
445
489
  handler: ({ agent, rawInput }) => {
446
490
  const state = stateFor(agent)
@@ -458,7 +502,13 @@ export function apply(ctx) {
458
502
  if (value === 'plan' || value === '') {
459
503
  return { kind: 'success', text: state.plan === null ? '还没有可展示的路由方案。' : JSON.stringify(state.plan) }
460
504
  }
461
- return { kind: 'error', text: '用法:/router mode collective、/router mode single 或 /router plan' }
505
+ if (value === 'safety' || value === 'approval') {
506
+ return { kind: 'success', text: JSON.stringify({ ...approvalGateStatus(ctx), context: approvalSafetyContext(state, state.lastStep) }) }
507
+ }
508
+ if (value === 'web' || value === 'network') {
509
+ return { kind: 'success', text: JSON.stringify({ ...webCapabilityStatus(ctx), context: webCapabilityForPlan(state.taskText, state.plan) }) }
510
+ }
511
+ return { kind: 'error', text: '用法:/router mode collective、/router mode single、/router plan、/router safety 或 /router web' }
462
512
  },
463
513
  })
464
514
 
@@ -515,7 +565,7 @@ export function apply(ctx) {
515
565
  await routerSettingsPromise
516
566
  state.taskText = inputText(messages)
517
567
  const liveBench = await liveBenchFor(ctx, state)
518
- state.plan = buildPlan({
568
+ const plan = buildPlan({
519
569
  text: state.taskText,
520
570
  available,
521
571
  mode: state.mode,
@@ -526,6 +576,16 @@ export function apply(ctx) {
526
576
  cacheReadRatio: routerSettings.cacheReadRatio,
527
577
  cacheWriteRatio: routerSettings.cacheWriteRatio,
528
578
  })
579
+ state.plan = {
580
+ ...plan,
581
+ web: webCapabilityForPlan(state.taskText, plan),
582
+ safety: {
583
+ ...approvalGateStatus(ctx),
584
+ ...approvalSafetyContext({ ...state, plan }, Number(step)),
585
+ hardCategories: ['deletion', 'credential', 'remote', 'system', 'bulk'],
586
+ failSafe: true,
587
+ },
588
+ }
529
589
  state.collaboration = shouldCollaborate(state.plan, available)
530
590
  ? { lastStep: 0, queuedStep: null }
531
591
  : null
@@ -538,11 +598,13 @@ export function apply(ctx) {
538
598
  const currentStep = Number.isFinite(Number(step)) ? Number(step) : state.lastStep + 1
539
599
  const stageContext = stageMessage(state.plan, currentStep)
540
600
  const analysisContext = currentStep === 1 ? analysisMessage(state.plan) : null
601
+ const webContext = currentStep === 1 ? webMessage(state.plan) : null
541
602
  const hasPersona = proposed.messages.some(message => message?.content?.some(block => isPersonaPrompt(block?.text)))
542
603
  if (hasPersona) state.personaInjected = true
543
604
  const personaContext = state.personaInjected ? null : personaMessage(state, agent, currentStep)
544
605
  const additions = []
545
606
  if (analysisContext !== null && !hasStageMarker(proposed.messages, currentStep)) additions.push(analysisContext)
607
+ if (webContext !== null && !proposed.messages.some(message => message?.content?.some(block => block?.text === webContext.content[0].text))) additions.push(webContext)
546
608
  if (personaContext !== null) {
547
609
  additions.push(personaContext)
548
610
  state.personaInjected = true
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Compatibility bridge for dsh-approval-gate.
3
+ *
4
+ * The approval-gate package owns the approval waterfall, Flash judgement,
5
+ * learning, audit files, snapshots and UI. This module only produces a small,
6
+ * deterministic safety context for the current Model Router work package.
7
+ * Keeping the bridge stateless makes it safe when the gate is installed by
8
+ * another profile layer as well as when it is bundled by this plugin.
9
+ */
10
+
11
+ export const APPROVAL_GATE_PACKAGE = '@ljwei-stak/dsh-approval-gate'
12
+ export const APPROVAL_GATE_VERSION = '0.5.2'
13
+
14
+ const ESCALATION_RE = /escalate\s+sandbox\s+to\s+([^\s:]+):?\s*([\s\S]*)/i
15
+
16
+ function clean(value, max = 240) {
17
+ return String(value ?? '').replace(/[\r\n]+/g, ' ').trim().slice(0, max)
18
+ }
19
+
20
+ function stageFor(state, step) {
21
+ const plan = state?.plan
22
+ const tasks = Array.isArray(plan?.subtasks) ? plan.subtasks : []
23
+ if (tasks.length === 0) return null
24
+ const index = Math.max(0, Number(step || state?.lastStep || 1) - 1)
25
+ return tasks[index] ?? tasks[tasks.length - 1] ?? null
26
+ }
27
+
28
+ /**
29
+ * Return the router safety facts that are relevant to an approval request.
30
+ * `bulk` is a candidate supplied as context; dsh-approval-gate still performs
31
+ * the authoritative category decision and fail-safe human handoff.
32
+ */
33
+ export function approvalSafetyContext(state, step) {
34
+ const plan = state?.plan
35
+ const tasks = Array.isArray(plan?.subtasks) ? plan.subtasks : []
36
+ const stage = stageFor(state, step)
37
+ const collective = state?.mode === 'collective'
38
+ const multiTask = collective && plan?.complexity?.band === 'complex' && tasks.length >= 3
39
+ return {
40
+ mode: state?.mode ?? 'collective',
41
+ complexity: plan?.complexity?.band ?? 'unknown',
42
+ stage: stage?.id ?? null,
43
+ stagePurpose: stage?.purpose ?? null,
44
+ stageType: stage?.type ?? null,
45
+ stageIndex: tasks.length === 0 ? 0 : Math.max(1, Number(step || state?.lastStep || 1)),
46
+ stageCount: tasks.length,
47
+ multiTask,
48
+ candidateCategory: multiTask ? 'bulk' : 'neutral',
49
+ selectedRoute: plan?.selected?.provider && plan?.selected?.model
50
+ ? `${plan.selected.provider}/${plan.selected.model}`
51
+ : null,
52
+ activeRoute: state?.lastTarget?.provider && state?.lastTarget?.model
53
+ ? `${state.lastTarget.provider}/${state.lastTarget.model}`
54
+ : null,
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Decorate the justification consumed by dsh-approval-gate. The original
60
+ * escalation prefix remains intact, so the target plugin can parse it. The
61
+ * marker is deliberately plain text because the target plugin's Flash model
62
+ * judges only the justification string.
63
+ */
64
+ export function decorateApprovalReason(reason, context) {
65
+ const raw = String(reason ?? '')
66
+ const match = raw.match(ESCALATION_RE)
67
+ if (!match || context === null || context === undefined) return raw
68
+ const mode = clean(match[1], 64)
69
+ const justification = clean(match[2], 500)
70
+ const stage = context.stage ? `${context.stage} ${context.stageIndex}/${context.stageCount}` : 'unknown'
71
+ const route = context.activeRoute || context.selectedRoute || 'unassigned'
72
+ const marker = [
73
+ '[model-router safety context]',
74
+ `mode=${context.mode}`,
75
+ `complexity=${context.complexity}`,
76
+ `stage=${stage}`,
77
+ `purpose=${context.stagePurpose || 'unknown'}`,
78
+ `route=${route}`,
79
+ `task_count=${context.stageCount || 0}`,
80
+ `risk_candidate=${context.candidateCategory}`,
81
+ context.multiTask ? 'multi_task_review=required' : 'multi_task_review=not_applicable',
82
+ ].join('; ')
83
+ return `escalate sandbox to ${mode}: ${justification || 'router stage requires sandbox escalation'} ${marker}`.trim()
84
+ }
85
+
86
+ export function isApprovalGateReason(reason) {
87
+ return ESCALATION_RE.test(String(reason ?? ''))
88
+ }
89
+
90
+ export function approvalGateStatus(ctx) {
91
+ let approval = false
92
+ let permissionPresets = false
93
+ try {
94
+ approval = Boolean(ctx?.get?.('approval'))
95
+ permissionPresets = Boolean(ctx?.get?.('permissionPresets'))
96
+ } catch {
97
+ approval = false
98
+ permissionPresets = false
99
+ }
100
+ return {
101
+ package: APPROVAL_GATE_PACKAGE,
102
+ version: APPROVAL_GATE_VERSION,
103
+ bundled: true,
104
+ approvalServiceDetected: approval,
105
+ permissionPresetsDetected: permissionPresets,
106
+ policy: 'hard-risk-human-review',
107
+ }
108
+ }
109
+
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Web capability selection for the bundled ModSearch + Ego Browser pair.
3
+ *
4
+ * ModSearch owns search/fetch engines and SSRF protection. Ego Browser owns
5
+ * the real visible browser, login state, screenshots, and human-check handoff.
6
+ * This module only classifies intent and creates an auditable instruction for
7
+ * the model; it never fetches an untrusted URL itself.
8
+ */
9
+
10
+ export const MODSEARCH_PACKAGE = '@liustack/modsearch'
11
+ export const MODSEARCH_VERSION = '5.10.1'
12
+ export const EGO_BROWSER_PACKAGE = 'dsh-ego-browser'
13
+ export const EGO_BROWSER_VERSION = '0.8.0'
14
+
15
+ const WEB_RE = /https?:\/\/|www\.|联网|上网|网页|网站|搜索|查找|资料|文献|新闻|最新|实时|网页内容|页面|来源|引用|网络|x\s*帖子|twitter|推特|github|反爬|验证码|登录|人机验证/i
16
+ const BROWSER_RE = /反爬|验证码|人机验证|cloudflare|turnstile|recaptcha|hcaptcha|登录|需要点击|动态页面|网页窗口|浏览器|可见窗口|手动验证|页面交互/i
17
+
18
+ function text(value, max = 12000) {
19
+ return String(value ?? '').slice(-max)
20
+ }
21
+
22
+ export function classifyWebIntent(input) {
23
+ const value = text(input)
24
+ const needsWeb = WEB_RE.test(value)
25
+ const directBrowser = BROWSER_RE.test(value)
26
+ return {
27
+ needsWeb,
28
+ directBrowser,
29
+ antiBotFallback: needsWeb,
30
+ primary: needsWeb ? 'modsearch' : 'native-model',
31
+ fallback: needsWeb ? 'ego-browser' : null,
32
+ reason: directBrowser
33
+ ? '用户明确要求动态网页、登录态或反爬页面,优先使用可见 Ego Browser。'
34
+ : needsWeb
35
+ ? '先使用 ModSearch 搜索/抓取;失败、内容不完整或触发人机验证时切换 Ego Browser。'
36
+ : '',
37
+ }
38
+ }
39
+
40
+ export function webCapabilityForPlan(input, plan = null) {
41
+ const intent = classifyWebIntent(input)
42
+ const tasks = Array.isArray(plan?.subtasks) ? plan.subtasks : []
43
+ return {
44
+ ...intent,
45
+ taskCount: tasks.length,
46
+ stageAware: tasks.length > 1,
47
+ packages: {
48
+ search: MODSEARCH_PACKAGE,
49
+ browser: EGO_BROWSER_PACKAGE,
50
+ },
51
+ versions: {
52
+ search: MODSEARCH_VERSION,
53
+ browser: EGO_BROWSER_VERSION,
54
+ },
55
+ humanCheckPolicy: 'pause-and-handoff',
56
+ }
57
+ }
58
+
59
+ export function webCapabilityStatus(ctx) {
60
+ let web = false
61
+ let tools = false
62
+ try {
63
+ web = Boolean(ctx?.get?.('web'))
64
+ tools = Boolean(ctx?.get?.('tools'))
65
+ } catch {
66
+ web = false
67
+ tools = false
68
+ }
69
+ return {
70
+ modsearch: { package: MODSEARCH_PACKAGE, version: MODSEARCH_VERSION, bundled: true, webServiceDetected: web },
71
+ egoBrowser: { package: EGO_BROWSER_PACKAGE, version: EGO_BROWSER_VERSION, bundled: true, toolServiceDetected: tools },
72
+ antiBotWindow: 'ego_space_open -> ego_navigate -> ego_page_info/ego_captcha -> ego_snapshot/ego_screenshot',
73
+ humanCheck: 'pause-and-handoff',
74
+ }
75
+ }
76
+
77
+ export function webInstruction(capability) {
78
+ if (!capability?.needsWeb) return ''
79
+ const browserFirst = capability.directBrowser
80
+ return [
81
+ '[联网与可见浏览器适配]',
82
+ browserFirst
83
+ ? '本任务涉及动态网页、登录态或反爬页面:优先使用 Ego Browser 的真实可见窗口。'
84
+ : '普通联网先使用 ModSearch 提供的 web_search/read_page;需要 X 内容时使用 x_search。',
85
+ '如果搜索或抓取返回 unavailable、内容不完整、JS 页面空白或 warnings,切换 Ego Browser:先 ego_space_open,再 ego_navigate;随后调用 ego_page_info 或 ego_captcha 检查 humanCheck。',
86
+ '页面可读时使用 ego_snapshot、ego_read_element、ego_screenshot 或 ego_http(browser) 提取证据;必须交互时使用 ego_click、ego_fill、ego_wait*。',
87
+ '检测到验证码、Cloudflare、Turnstile、登录或其他 humanCheck=true 时,暂停当前工作包,提示用户在 Agent 浏览器观察窗完成验证;用户确认后再继续。不要绕过验证码或伪造验证结果。',
88
+ '联网证据必须保留 URL、页面状态和不确定性说明;批量抓取仍遵守审批门控。',
89
+ ].join('\n')
90
+ }