dsh-feiyu-mood 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## 0.8.4 — 2026-11-01
2
+
3
+ **「检查更新」以前会报错版本,而且只给一条命令让你自己手动复制、然后手动装**
4
+
5
+ - 🐞 **"最新版本"会骗人**:以前是「哪个地址先返回就用哪个」。GitHub 一超时就落到 jsDelivr ——
6
+ 那个地址**带 CDN 缓存、会滞后好几天**,于是同一分钟内能报出好几个不同的"最新",
7
+ 甚至比你本机装的还旧,看着像"该降级"。
8
+ - ✅ 现在**三个源全查**(GitHub / jsDelivr / npm registry),**取版本号最大的那个**:
9
+ 版本只会往前走,哪个源报小了就是它滞后。三个都失败才说"查不到",绝不把旧值当成"最新"。
10
+ - ✨ **检测到新版可以直接装**:那一块多了一个「一键更新到 vX」按钮,点它走 DSH 自己的插件管理装最新版,
11
+ 装完会提示你重启(或重载)一次。以前只有一个「复制命令」,得你自己去终端敲两行。
12
+ - 🧹 **界面上那些命令行全删了**:以前那两行 `dsh plugin --profile web remove/add` 和「复制命令」按钮不再显示 —— 很多人看不懂,还得跑来问。现在有新版就只有「一键更新到 vX」一个按钮。
13
+ - ⚠ **本地开发副本(link: 进来的那种)不给一键装** —— 那会把正在改的目录换成仓库里的版本;
14
+ 这种会明确写出来,让你手动更新。
15
+
1
16
  ## 0.8.3 — 2026-10-31
2
17
 
3
18
  **外面那个挂件开始跟着心情换形象(五档)**
package/client.js CHANGED
@@ -186,7 +186,19 @@ window.__ModuleLoader__.load({
186
186
  }
187
187
  setUpBusy('')
188
188
  }
189
- const 更新命令 = ['dsh plugin --profile web remove dsh-feiyu-mood', 'dsh plugin --profile web add dsh-feiyu-mood'].join(String.fromCharCode(10))
189
+
190
+ // ⭐ 2026-11-01:查到新版就**一键装**(宿主那边的 pluginManager.installBundle)
191
+ const 一键更新 = async () => {
192
+ setUpBusy('1')
193
+ try {
194
+ const r = await postJson(API + '/install', {})
195
+ if (r && r.ok) flash('开始更新了 —— 装完按提示重载一次')
196
+ else flash('更新失败:' + ((r && r.error) || '不知道原因'))
197
+ } catch (e) {
198
+ flash('更新失败:' + (e && e.message))
199
+ }
200
+ setUpBusy('')
201
+ }
190
202
 
191
203
  /* ⭐ 一份心情可以绑好几个会话(或一个工作区):这里照这份清单画
192
204
  (没配置过就当成「第一份」;id 尽量沿用宿主给的 st.key,别凭空造一个) */
@@ -271,15 +283,14 @@ window.__ModuleLoader__.load({
271
283
  upBusy === '1' ? '查…' : '检查更新'))
272
284
  const 新版块 = (up && up.hasUpdate)
273
285
  ? h('div', { 'data-fm-sec': '' },
274
- h('div', { 'data-fm-note': '' }, '更新方法:先卸旧版,再装新版 —— 直接装可能拿到缓存里的旧副本。'),
275
- h('div', { 'data-fm-quote': '' }, 更新命令),
286
+ h('div', { 'data-fm-note': '' }, up.canInstall
287
+ ? '仓库里已经有 v' + up.latest + ' 了,点下面这个按钮直接装。'
288
+ : '仓库里已经有 v' + up.latest + ' 了。这是本地开发副本(link: 进来的),请手动更新。'),
276
289
  h('div', { 'data-fm-row': '' },
277
- h('button', {
278
- 'data-fm-btn': '',
279
- onClick: () => navigator.clipboard.writeText(更新命令)
280
- .then(() => flash('命令已复制'))
281
- .catch(() => flash('复制不了,手动选中吧')),
282
- }, '复制命令'),
290
+ up.canInstall
291
+ ? h('button', { 'data-fm-btn': '', disabled: upBusy === '1', onClick: 一键更新 },
292
+ upBusy === '1' ? '装…' : '一键更新到 v' + up.latest)
293
+ : null,
283
294
  up.repoUrl
284
295
  ? h('a', {
285
296
  'data-fm-btn': '', href: up.repoUrl, target: '_blank', rel: 'noreferrer',
@@ -1,17 +1,22 @@
1
- // 插件自查更新 —— 查的是**这个插件自己的仓库**
1
+ // 插件自查更新 —— 查的是**这个插件自己的仓库**,并且能一键装
2
2
  //
3
- // 每个插件在自己设置页放一个「检查更新」,点一下就去自己的仓库看 package.json 里的版本。
3
+ // 每个插件在自己设置页放一个「检查更新」:点一下去自己的仓库 / npm 看最新版本。
4
4
  //
5
5
  // 原则:
6
6
  // · 仓库地址从包里自己的 package.json 读(`repository`),不在代码里写死
7
- // · 只查,不装不删 —— 更新命令交给用户自己复制去跑
8
- // · **查不到要如实说"查不到",绝不能显示成"已是最新"**
7
+ // · **多源全查、取最大的那个版本** —— 以前是"谁先返回用谁",GitHub 一超时就落到
8
+ // jsDelivr 那个**带 CDN 缓存**的源上,于是同一分钟能报出两个不同的"最新",甚至比本机还旧
9
+ // · 查不到要如实说"查不到",绝不能显示成"已是最新"
9
10
  // · 结果缓存 6 小时;点按钮(refresh)就绕过缓存
11
+ // · 「一键更新」走宿主自己的 pluginManager.installBundle —— 装完由宿主决定要不要重载;
12
+ // **本地开发副本(link:)不给装**,那会把她正在改的目录换成仓库里的版本
10
13
  //
11
- // ⚠ 内容会进客户端的一份说明:这个文件是**复制多份**到各个插件里的同一份实现,
12
- // 只在这一个地方改了,记得同步给其它插件(别各改各的)。
14
+ // ⚠ 这个文件是**复制多份**到各个插件里的同一份实现,只在这一个地方改了要同步给其它插件。
15
+ //
16
+ // 2026-11-01:多源取最大 + 一键更新(install)+ 装完把结果原样带回界面。
13
17
 
14
- import { readFileSync } from 'node:fs'
18
+ import { readFileSync, readdirSync, existsSync } from 'node:fs'
19
+ import { homedir } from 'node:os'
15
20
  import { join } from 'node:path'
16
21
 
17
22
  const TTL = 6 * 3600 * 1000
@@ -39,17 +44,23 @@ export function cmpVer(a, b) {
39
44
  return x.pre > y.pre ? 1 : -1
40
45
  }
41
46
 
42
- function rawUrls(repo) {
47
+ // 三个源都要查:GitHub raw(真值)· jsDelivr(有缓存,可能滞后)· npm registry(发过就有)
48
+ function sources(meta, repo) {
49
+ const out = []
43
50
  const s = String(repo || '').trim()
44
51
  const m = /github\.com[/:]([^/]+)\/([^/#?]+)/i.exec(s)
45
- if (!m) return []
46
- const owner = m[1]
47
- const name = m[2].replace(/\.git$/i, '')
48
- return [
49
- `https://raw.githubusercontent.com/${owner}/${name}/HEAD/package.json`,
50
- // 备用:jsDelivr 也能读 GitHub,但**有缓存**,可能滞后 —— 消息里会标出来
51
- `https://cdn.jsdelivr.net/gh/${owner}/${name}@main/package.json`,
52
- ]
52
+ if (m) {
53
+ const owner = m[1]
54
+ const name = m[2].replace(/\.git$/i, '')
55
+ out.push({ url: `https://raw.githubusercontent.com/${owner}/${name}/HEAD/package.json`, from: 'GitHub' })
56
+ out.push({ url: `https://cdn.jsdelivr.net/gh/${owner}/${name}@main/package.json`, from: 'jsDelivr' })
57
+ }
58
+ const pkg = String((meta && meta.name) || '').trim()
59
+ if (pkg) {
60
+ const enc = pkg.startsWith('@') ? '@' + pkg.slice(1).replace('/', '%2F') : encodeURIComponent(pkg)
61
+ out.push({ url: `https://registry.npmjs.org/${enc}/latest`, from: 'npm' })
62
+ }
63
+ return out
53
64
  }
54
65
 
55
66
  export function createSelfUpdate({ packageRoot, diag }) {
@@ -73,30 +84,40 @@ export function createSelfUpdate({ packageRoot, diag }) {
73
84
  .replace(/\.git$/, '')
74
85
  .replace(/#readme$/i, '')
75
86
 
87
+ // 装在自己的 node_modules 里 = 正常安装;不在 = 本地 link 的开发副本
88
+ const 是开发副本 = !/[\\/]node_modules[\\/]/.test(String(packageRoot))
89
+
76
90
  let cache = null
77
91
 
78
92
  async function fetchLatest() {
79
- const urls = rawUrls(repo)
80
- if (!urls.length) return { latest: null, error: '包里没写 GitHub 仓库地址,没法查' }
81
- let lastErr = ''
82
- for (const url of urls) {
93
+ const list = sources(meta, repo)
94
+ if (!list.length) return { latest: null, error: '包里没写仓库地址,也不像在 npm 上发过,没法查' }
95
+ const 结果 = await Promise.all(list.map(async (s) => {
83
96
  try {
84
- const res = await fetch(url, {
97
+ const res = await fetch(s.url, {
85
98
  signal: AbortSignal.timeout(TIMEOUT),
86
99
  headers: { accept: 'application/json', 'user-agent': 'dsh-plugin-selfupdate' },
87
100
  })
88
- if (!res.ok) { lastErr = 'HTTP ' + res.status; continue }
101
+ if (!res.ok) return { err: s.from + ' HTTP ' + res.status }
89
102
  const j = await res.json()
90
- if (!j || !j.version) { lastErr = '仓库里的 package.json 没有 version'; continue }
91
- return {
92
- latest: String(j.version),
93
- from: url.includes('jsdelivr') ? 'jsDelivr(有缓存,可能滞后)' : 'GitHub',
94
- }
103
+ const v = String((j && j.version) || '')
104
+ if (!v) return { err: s.from + ' 没有 version' }
105
+ return { v, from: s.from }
95
106
  } catch (e) {
96
- lastErr = String((e && e.message) || e)
107
+ return { err: s.from + ' ' + String((e && e.message) || e) }
97
108
  }
109
+ }))
110
+ // ⭐ 取**最大**的那个:版本只会往前走,任何一个源报小了都只是它滞后
111
+ let best = null
112
+ let bestFrom = null
113
+ for (const r of 结果) {
114
+ if (!r.v) continue
115
+ if (best === null || cmpVer(r.v, best) > 0) { best = r.v; bestFrom = r.from }
98
116
  }
99
- return { latest: null, error: lastErr || '查询失败' }
117
+ if (best === null) {
118
+ return { latest: null, error: 结果.map((r) => r.err).filter(Boolean).join(' · ') || '查询失败' }
119
+ }
120
+ return { latest: best, from: bestFrom }
100
121
  }
101
122
 
102
123
  async function check(force) {
@@ -113,32 +134,90 @@ export function createSelfUpdate({ packageRoot, diag }) {
113
134
  error: r.error || null,
114
135
  from: r.from || null,
115
136
  repoUrl: repoUrl || null,
137
+ devCopy: 是开发副本,
138
+ canInstall: !是开发副本,
116
139
  checkedAt: Date.now(),
117
140
  }
118
- log('查更新:当前 ' + current + ' / 最新 ' + (r.latest || '查不到') + (r.error ? '(' + r.error + ')' : ''))
141
+ log('查更新:当前 ' + current + ' / 最新 ' + (r.latest || '查不到') + (r.from ? '(' + r.from + ')' : '') + (r.error ? '(' + r.error + ')' : ''))
119
142
  return cache
120
143
  }
121
144
 
145
+ // 这个插件是以什么身份装的?从 profile 的 package.json 里把它那行依赖原样取出来 ——
146
+ // 用它当 spec 才是"同源更新",不会把 github 装的换成 npm 版。
147
+ function 取安装来源() {
148
+ const HOME = process.env.DSH_HOME || join(homedir(), '.dsh')
149
+ const name = String(meta.name || '')
150
+ if (!name) return ''
151
+ let profiles = []
152
+ try { profiles = readdirSync(join(HOME, 'profiles')) } catch (e) { return '' }
153
+ for (const p of profiles) {
154
+ const f = join(HOME, 'profiles', p, 'package.json')
155
+ if (!existsSync(f)) continue
156
+ try {
157
+ const j = JSON.parse(readFileSync(f, 'utf8'))
158
+ const dep = String((j.dependencies && j.dependencies[name]) || '')
159
+ if (/^(github:|git\+|https?:)/i.test(dep)) return dep
160
+ } catch (e) { /* 读不了就下一个 */ }
161
+ }
162
+ // 兜底:从 package.json 的仓库地址拼
163
+ const m = /github\.com[/:]([^/]+)\/([^/#?]+)/i.exec(repoUrl)
164
+ if (m) return 'github:' + m[1] + '/' + m[2].replace(/\.git$/i, '')
165
+ return ''
166
+ }
167
+
168
+ async function install(ctx) {
169
+ if (是开发副本) {
170
+ return { ok: false, error: '这是本地开发副本(link: 进来的),一键更新会把你正在改的目录换成仓库里的版本 —— 要更新请在仓库里 git pull' }
171
+ }
172
+ const pm = ctx && ctx.pluginManager
173
+ if (!pm || typeof pm.installBundle !== 'function') {
174
+ return { ok: false, error: '这版 DSH 没有插件管理接口,装不了;请手动更新' }
175
+ }
176
+ const spec = 取安装来源() || (meta.name ? String(meta.name) : '')
177
+ if (!spec) return { ok: false, error: '不知道该从哪儿装(既没有仓库地址,也没记录安装来源)' }
178
+ try {
179
+ log('一键更新:开始装 ' + spec)
180
+ const r = await pm.installBundle(spec)
181
+ log('一键更新:' + JSON.stringify(r && (r.applied ?? r.application ?? r.reason ?? r)))
182
+ return { ok: true, spec, result: r || null }
183
+ } catch (e) {
184
+ log('一键更新失败:' + String((e && e.message) || e))
185
+ return { ok: false, spec, error: String((e && e.message) || e) }
186
+ }
187
+ }
188
+
122
189
  function register(ctx, route) {
190
+ const send = (res, code, obj) => {
191
+ res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
192
+ res.end(JSON.stringify(obj))
193
+ }
123
194
  try {
124
195
  ctx.effect(() => ctx.webServer.register({
125
196
  kind: 'exact',
126
197
  path: route + '/update',
127
198
  handler: async (req, res) => {
128
- const send = (code, obj) => {
129
- res.writeHead(code, {
130
- 'content-type': 'application/json; charset=utf-8',
131
- 'cache-control': 'no-store',
132
- })
133
- res.end(JSON.stringify(obj))
134
- }
135
199
  try {
136
200
  const url = new URL(req.url, 'http://127.0.0.1')
137
201
  const force = url.searchParams.get('refresh') === '1'
138
- send(200, { ok: true, ...(await check(force)) })
202
+ send(res, 200, { ok: true, ...(await check(force)) })
139
203
  } catch (e) {
140
204
  log('查更新路由失败:' + (e && e.message))
141
- send(200, { ok: false, current, latest: null, error: String((e && e.message) || e) })
205
+ send(res, 200, { ok: false, current, latest: null, error: String((e && e.message) || e) })
206
+ }
207
+ },
208
+ }))
209
+ // ⭐ 一键更新:POST。装完的结果原样带回去(可能要重启才生效)
210
+ ctx.effect(() => ctx.webServer.register({
211
+ kind: 'exact',
212
+ path: route + '/install',
213
+ handler: async (req, res) => {
214
+ if (req.method !== 'POST') { send(res, 405, { ok: false, error: '只接受 POST' }); return }
215
+ try {
216
+ const r = await install(ctx)
217
+ send(res, 200, r)
218
+ } catch (e) {
219
+ log('一键更新路由失败:' + (e && e.message))
220
+ send(res, 200, { ok: false, error: String((e && e.message) || e) })
142
221
  }
143
222
  },
144
223
  }))
@@ -147,5 +226,5 @@ export function createSelfUpdate({ packageRoot, diag }) {
147
226
  }
148
227
  }
149
228
 
150
- return { current, repo, repoUrl, check, register }
229
+ return { current, repo, repoUrl, check, install, register }
151
230
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-feiyu-mood",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "给 AI 伙伴一个会自己判断、你看得见的心情:每轮聊完自动判断涨跌,右下角挂饰点开看数值 + 情绪 + 时间线;心情怎么告诉她(不告诉 / 只说心情 / 说她刚才那件事)和可信度门槛都能在设置里改。",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Yuuyuko-uu (https://github.com/Yuuyuko-uu)",