dsh-cerebrum 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/README.md CHANGED
@@ -57,6 +57,7 @@ dsh plugin --profile desktop add D:\workspace\dsh-cerebrum
57
57
 
58
58
  - **不喜欢可以关掉**:面板里的 🙈 隐藏芯片(点小 🧠 恢复);彻底关闭用配置 `ui.enabled: false`(重启宿主生效,芯片完全不出现);
59
59
  - 面板可**拖动**(按住标题栏)、可 **📌 固定**到输入栏上方成横条;
60
+ - **宠物联动(免配置)**:检测到同源的 `dsh-pet`(`/dsh-pet-7340`)时,情绪变化会自动推给桌宠——加锁 → 让宠物说一句大脑台词 → 释放锁;不需要用户配置。不想要联动可设 `ui.drivePet: false`;
60
61
  - 数据来自宿主内置的 127.0.0.1 只读 API(`http://127.0.0.1:18734`,端口可配 `config.api.port`);
61
62
  - UI 只读:修改记忆/情绪仍通过 `cerebrum_*` 工具;
62
63
  - 免构建 client bundle(`lib/client.js`),与宿主同一个安装包——**装一个插件,大脑 + 面板都有**。
package/docs/publish.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # 发布 dsh-cerebrum 到 DSH 插件市场
2
2
 
3
+ > **发布状态(2026-10-28)**
4
+ > - GitHub 公开仓库:https://github.com/liu-wen-wu/dsh-cerebrum(main + tag v0.1.1,SSH 推送)
5
+ > - npm:`dsh-cerebrum@0.1.2` 已上线(latest;0.1.1 上一版,含内置宠物桥接)
6
+ > - 市场收录申请:https://github.com/polang233/dsh-plugin-index/issues/121
7
+ > - 本地安装:`dsh-cerebrum@link:D:/workspace/dsh-cerebrum`(desktop profile,已热加载验证)
8
+
3
9
  > 目标市场:dshopen.org(DSH 应用内「插件市场/Discover」读取的社区索引,`https://dshopen.org/index.json`)。
4
10
  > 收录流程由 [dsh-plugin-index](https://github.com/polang233/dsh-plugin-index) 人工审核:**公开仓库 + 可安装的 npm 版本 + 明确许可证**,三样齐全后提 issue 收录。
5
11
 
package/lib/api.js CHANGED
@@ -18,40 +18,64 @@
18
18
  import http from 'node:http'
19
19
  import { INTERACTION_KINDS } from './core/petbrain.js'
20
20
 
21
- const cors = (res) => {
22
- res.setHeader('Access-Control-Allow-Origin', '*')
23
- res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS')
24
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
25
- res.setHeader('Content-Type', 'application/json; charset=utf-8')
21
+ const CORS_METHODS = 'GET, OPTIONS'
22
+ const LOOPBACK = /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/
23
+ const LOOPBACK_ORIGIN = /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/
24
+
25
+ /**
26
+ * Access control for the loopback API (review feedback on dshplugin-index#121):
27
+ * - Host must be a loopback host (blocks DNS rebinding).
28
+ * - If the request carries an Origin (cross-origin browser fetch), it must also
29
+ * be a loopback origin — an arbitrary website can no longer read the brain.
30
+ * - Optional shared token (`config.api.token`) for users who want a second lock.
31
+ */
32
+ const checkAccess = (req, token) => {
33
+ const host = String(req.headers.host ?? '')
34
+ if (!LOOPBACK.test(host)) return { ok: false, status: 403, reason: 'host-not-allowed' }
35
+ const origin = req.headers.origin
36
+ if (origin && !LOOPBACK_ORIGIN.test(String(origin))) return { ok: false, status: 403, reason: 'origin-not-allowed' }
37
+ if (token && req.headers['x-cerebrum-token'] !== token) return { ok: false, status: 401, reason: 'token-required' }
38
+ return { ok: true }
26
39
  }
27
40
 
28
- const send = (res, status, payload) => {
29
- cors(res)
41
+ const send = (res, status, payload, origin) => {
42
+ if (origin) res.setHeader('Access-Control-Allow-Origin', String(origin))
43
+ res.setHeader('Access-Control-Allow-Methods', CORS_METHODS)
44
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-cerebrum-token')
45
+ res.setHeader('Vary', 'Origin')
46
+ res.setHeader('Content-Type', 'application/json; charset=utf-8')
30
47
  res.statusCode = status
31
48
  res.end(JSON.stringify(payload))
32
49
  }
33
50
 
34
- export const startBrainApi = (service, { port = 18734, ui = { enabled: true }, logger = console } = {}) => {
51
+ export const startBrainApi = (service, { port = 18734, ui = { enabled: true }, token = '', logger = console } = {}) => {
52
+ // Interaction rate limit: /interact is the only mutation; cap it to stop flooding.
53
+ let interactWindow = { at: 0, count: 0 }
35
54
  const server = http.createServer((req, res) => {
36
- if (req.method === 'OPTIONS') return send(res, 204, {})
37
- if (req.method !== 'GET') return send(res, 405, { error: 'method not allowed' })
55
+ const origin = req.headers.origin
56
+ const allowedOrigin = origin && LOOPBACK_ORIGIN.test(String(origin)) ? String(origin) : null
57
+ const access = checkAccess(req, token)
58
+ if (!access.ok) return send(res, access.status, { error: access.reason }, allowedOrigin)
59
+ if (req.method === 'OPTIONS') return send(res, 204, {}, allowedOrigin)
60
+ if (req.method !== 'GET') return send(res, 405, { error: 'method not allowed' }, allowedOrigin)
38
61
  const url = new URL(req.url, `http://127.0.0.1:${port}`)
62
+ const reply = (status, payload) => send(res, status, payload, allowedOrigin)
39
63
  try {
40
64
  switch (url.pathname) {
41
65
  case '/ping':
42
- return send(res, 200, { ok: true, name: 'dsh-cerebrum', uptimeMs: Date.now() - service.startedAt })
66
+ return reply(200, { ok: true, name: 'dsh-cerebrum', uptimeMs: Date.now() - service.startedAt })
43
67
  case '/ui':
44
68
  // The client respects this: ui.enabled=false removes the chip entirely.
45
- return send(res, 200, { enabled: ui.enabled !== false })
69
+ return reply(200, { enabled: ui.enabled !== false, drivePet: ui.drivePet !== false })
46
70
  case '/state': {
47
71
  const sessionId = url.searchParams.get('session') || undefined
48
- return send(res, 200, service.state(sessionId))
72
+ return reply(200, service.state(sessionId))
49
73
  }
50
74
  case '/recall': {
51
75
  const query = url.searchParams.get('q') ?? ''
52
76
  const sessionId = url.searchParams.get('session') || undefined
53
77
  const hits = service.recall(query, { k: 3, sessionId })
54
- return send(res, 200, {
78
+ return reply(200, {
55
79
  query,
56
80
  memories: hits.map((hit) => ({
57
81
  summary: hit.episode.summary,
@@ -63,21 +87,25 @@ export const startBrainApi = (service, { port = 18734, ui = { enabled: true }, l
63
87
  }
64
88
  case '/render-hints': {
65
89
  const sessionId = url.searchParams.get('session') || undefined
66
- return send(res, 200, service.renderHintsFor(sessionId))
90
+ return reply(200, service.renderHintsFor(sessionId))
67
91
  }
68
92
  case '/interact': {
69
93
  // The only mutation the API allows: affect-only pet interactions.
94
+ const now = Date.now()
95
+ if (now - interactWindow.at > 60_000) interactWindow = { at: now, count: 0 }
96
+ interactWindow.count++
97
+ if (interactWindow.count > 30) return reply(429, { error: 'too-many-interactions' })
70
98
  const kind = url.searchParams.get('kind') ?? ''
71
99
  const sessionId = url.searchParams.get('session') || undefined
72
100
  const hints = service.interact(kind, sessionId)
73
- if (!hints) return send(res, 400, { error: `unknown interaction kind, use one of ${INTERACTION_KINDS.join(', ')}` })
74
- return send(res, 200, hints)
101
+ if (!hints) return reply(400, { error: `unknown interaction kind, use one of ${INTERACTION_KINDS.join(', ')}` })
102
+ return reply(200, hints)
75
103
  }
76
104
  default:
77
- return send(res, 404, { error: 'unknown endpoint' })
105
+ return reply(404, { error: 'unknown endpoint' })
78
106
  }
79
107
  } catch (error) {
80
- return send(res, 500, { error: error?.message ?? 'internal error' })
108
+ return reply(500, { error: error?.message ?? 'internal error' })
81
109
  }
82
110
  })
83
111
 
package/lib/client.js CHANGED
@@ -86,17 +86,82 @@ window.__ModuleLoader__.load({
86
86
  })
87
87
  var panelRef = useRef(null)
88
88
  var dragRef = useRef(null)
89
+ var petRef = useRef({ key: '', at: 0 })
90
+ var uiRef = useRef({ enabled: true, drivePet: true })
89
91
 
90
92
  useEffect(function () {
91
93
  var alive = true
92
94
  // The host decides whether users opted out of the whole chip.
93
95
  get('/ui').then(function (data) {
94
- if (alive && data && data.enabled === false) setUiEnabled(false)
96
+ if (data) {
97
+ uiRef.current = { enabled: data.enabled !== false, drivePet: data.drivePet !== false }
98
+ if (!alive) return
99
+ if (data.enabled === false) setUiEnabled(false)
100
+ }
95
101
  })
102
+
103
+ /**
104
+ * Built-in pet bridge: no setup required. When a dsh-pet instance is
105
+ * detected on this origin, emotion changes are pushed to it as a
106
+ * locked "performance" (bubble line), then the lock is released.
107
+ */
108
+ var drivePet = function (hints) {
109
+ if (uiRef.current.drivePet === false) return
110
+ var key = (hints.emotionKey || '') + '|' + (hints.line || '')
111
+ var now = Date.now()
112
+ if (petRef.current.key === key) return
113
+ if (now - petRef.current.at < 20000) return
114
+ if ((hints.intensity || 0) < 0.25) return
115
+ petRef.current.key = key
116
+ petRef.current.at = now
117
+ fetch('/dsh-pet-7340/state')
118
+ .then(function (r) { return r.ok ? r.json() : null })
119
+ .then(function (st) {
120
+ if (!st || !st.pets) return
121
+ var id = Object.keys(st.pets)[0]
122
+ if (!id) return
123
+ var attempt = function (surface) {
124
+ var q = '?pet=' + encodeURIComponent(id) + '&surface=' + surface
125
+ return fetch('/dsh-pet-7340/lock' + q, {
126
+ method: 'POST',
127
+ headers: { 'Content-Type': 'application/json' },
128
+ body: JSON.stringify({ ttl: 8000, surface: surface }),
129
+ })
130
+ .then(function (r) { return r.json() })
131
+ .then(function (lock) {
132
+ if (!lock || lock.ok === false || !lock.lockId) {
133
+ if (surface === 'web') return attempt('desktop')
134
+ return
135
+ }
136
+ var lq = q + '&lock=' + encodeURIComponent(lock.lockId)
137
+ return fetch('/dsh-pet-7340/broadcast' + lq, {
138
+ method: 'POST',
139
+ headers: { 'Content-Type': 'application/json' },
140
+ body: JSON.stringify({ text: hints.line, duration: 6000 }),
141
+ })
142
+ .catch(function () {})
143
+ .then(function () {
144
+ return fetch('/dsh-pet-7340/lock/release' + q, {
145
+ method: 'POST',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify({ lockId: lock.lockId }),
148
+ }).catch(function () {})
149
+ })
150
+ })
151
+ }
152
+ return attempt('web')
153
+ })
154
+ .catch(function () {})
155
+ }
156
+
96
157
  var tick = function () {
97
- get('/state' + (sessionId ? '?session=' + encodeURIComponent(sessionId) : '')).then(function (data) {
158
+ var qs = sessionId ? '?session=' + encodeURIComponent(sessionId) : ''
159
+ get('/state' + qs).then(function (data) {
98
160
  if (alive && data) setState(data)
99
161
  })
162
+ get('/render-hints' + qs).then(function (hints) {
163
+ if (alive && hints) drivePet(hints)
164
+ })
100
165
  }
101
166
  tick()
102
167
  var timer = setInterval(tick, POLL_MS)
package/lib/config.js CHANGED
@@ -45,10 +45,14 @@ export const DEFAULT_CONFIG = {
45
45
  api: {
46
46
  enabled: true,
47
47
  port: 18734,
48
+ /** Optional shared secret; when set, every request must send x-cerebrum-token. */
49
+ token: '',
48
50
  },
49
51
  /** Client UI (the brain chip/dashboard). Users who dislike it can switch it off. */
50
52
  ui: {
51
53
  enabled: true,
54
+ /** Built-in bridge: drive a detected dsh-pet with brain emotions. */
55
+ drivePet: true,
52
56
  },
53
57
  autoCapture: {
54
58
  enabled: true,
@@ -120,6 +124,7 @@ export const resolveConfig = (raw, { home = resolveHome(), cwd = process.cwd() }
120
124
  merged.api.enabled = merged.api.enabled !== false
121
125
  merged.api.port = Math.round(clampNumber(merged.api.port, 18734, 1024, 65535))
122
126
  merged.ui.enabled = merged.ui.enabled !== false
127
+ merged.ui.drivePet = merged.ui.drivePet !== false
123
128
  merged.autoCapture.maxTextChars = Math.round(clampNumber(merged.autoCapture.maxTextChars, 2000, 200, 20000))
124
129
  merged.autoCapture.toolResultChars = Math.round(clampNumber(merged.autoCapture.toolResultChars, 300, 40, 4000))
125
130
  merged.autoCapture.minChars = Math.round(clampNumber(merged.autoCapture.minChars, 6, 0, 500))
package/lib/index.js CHANGED
@@ -57,7 +57,7 @@ export function apply(ctx, rawConfig = {}) {
57
57
 
58
58
  // Read-only loopback API powering the optional client UI (dock + dashboard).
59
59
  if (config.api.enabled) {
60
- const api = startBrainApi(service, { port: config.api.port, ui: config.ui })
60
+ const api = startBrainApi(service, { port: config.api.port, ui: config.ui, token: config.api.token })
61
61
  ctx.effect(() => () => api.close(), 'cerebrum:brain-api')
62
62
  }
63
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cerebrum",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Brain-inspired cognitive architecture for DeepSeek Harness: hippocampal episodic memory, cortical semantic memory, spreading-activation recall, appraisal-based emotion, neuromodulator state, Big Five personality and dual-process judgment.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -90,6 +90,8 @@
90
90
  "forecast": "node examples/forecast.mjs",
91
91
  "pet": "node examples/pet-bridge.mjs",
92
92
  "pet-hook": "node examples/pet-hook.mjs",
93
- "privacy": "node scripts/privacy-check.mjs"
93
+ "privacy": "node scripts/privacy-check.mjs",
94
+ "release": "node scripts/release.mjs",
95
+ "market": "node scripts/check-market.mjs"
94
96
  }
95
97
  }