magicrew-auth 0.3.13 → 0.3.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.
@@ -1,100 +1,100 @@
1
- // 模型上下文窗口 —— 向 magpie 自己的模型库查
2
- //
3
- // ═══ 为什么不能自己填一个值 ═══════════════════════════════════════════
4
- //
5
- // magicrew 的两个模型接口**都不返回上下文窗口**:
6
- // GET /api/v1/flows/models → value / label / vision / tags / configs
7
- // GET /v1/models → id / object / created_at / owner_by
8
- //
9
- // 早前这里一律填 256_000 当保守值,后果是反的:
10
- // `pi-ai detected context overflow for model "magicrew/deepseek-v4.1-flash"`
11
- // —— agent 拿 256K 判断"放不下了",而实际窗口是 1M,白砍掉四分之三。
12
- //
13
- // magpie 自己有一份模型库(`~/.cache/magpie/models.json`,按厂商分组,
14
- // 每条带 `limit.context`),它本来就随 magpie 更新。**向它查,不要自己编。**
15
- //
16
- // 查不到才退回保守值 —— 宁可保守,不要给出超过真实窗口的数(那会让 agent
17
- // 以为还有余量,请求超了上游才报错)。
18
-
19
- import { readFileSync } from "node:fs"
20
- import { homedir } from "node:os"
21
- import { join } from "node:path"
22
-
23
- /**
24
- * 查不到时的兜底:**1M**。
25
- *
26
- * 优先级从高到低:
27
- * ① 用户在 magpie GUI 里手选的(「上下文窗口」那一行)—— magpie 直接用它,插件的值只是默认
28
- * ② 本文件从 magpie 模型库查到的真实值(models.dev 数据)
29
- * ③ 这个兜底
30
- *
31
- * ⚠ 兜底故意给大的:给 1M 而实际只有 128K 时,上游会在超限时报错,用户看得到;
32
- * 给 256K 而实际有 1M 时,agent 会主动拒绝一段本可以处理的对话 —— 那是静默的能力损失,
33
- * 比报错更难查(`pi-ai detected context overflow` 就是这么来的)。
34
- */
35
- const FALLBACK_CONTEXT = 1_048_576
36
-
37
- let cache = null
38
- let cacheAt = 0
39
-
40
- /** models.json 5-6MB,读一次缓存住;10 分钟内不重复读 */
41
- const CACHE_MS = 10 * 60_000
42
-
43
- function loadLibrary() {
44
- if (cache && Date.now() - cacheAt < CACHE_MS) return cache
45
- const idx = new Map()
46
- try {
47
- const raw = readFileSync(join(homedir(), ".cache", "magpie", "models.json"), "utf8")
48
- const walk = (o) => {
49
- if (Array.isArray(o)) {
50
- for (const v of o) walk(v)
51
- return
52
- }
53
- if (!o || typeof o !== "object") return
54
- const id = typeof o.id === "string" ? o.id : ""
55
- const ctx = o.limit?.context ?? o.context_length ?? o.context_window
56
- if (id && Number(ctx) > 0) {
57
- idx.set(id.toLowerCase(), Number(ctx))
58
- // 同一个模型可能挂在多个厂商下(deepseek-ai/x、Pro/deepseek-ai/x …),
59
- // 只保留最大值 —— 那才是它真能收的
60
- const tail = id.toLowerCase().split("/").pop()
61
- if (tail) {
62
- const prev = idx.get(`~${tail}`) || 0
63
- if (Number(ctx) > prev) idx.set(`~${tail}`, Number(ctx))
64
- }
65
- }
66
- for (const v of Object.values(o)) walk(v)
67
- }
68
- walk(JSON.parse(raw))
69
- } catch {}
70
- cache = idx
71
- cacheAt = Date.now()
72
- return idx
73
- }
74
-
75
- /**
76
- * 这个模型的上下文窗口。
77
- * @param {string} modelId 插件侧的模型 id,如 `deepseek-v4.1-flash`
78
- * @returns {number} 字节按 token 计;查不到给保守值
79
- */
80
- export function contextOf(modelId) {
81
- const id = String(modelId || "").toLowerCase()
82
- if (!id) return FALLBACK_CONTEXT
83
- const idx = loadLibrary()
84
-
85
- // ① 全名命中(插件 id 恰好等于库里的 id)
86
- if (idx.has(id)) return idx.get(id)
87
-
88
- // ② 尾段命中:库里的 `deepseek-ai/deepseek-v4.1-flash` ↔ 我们的 `deepseek-v4.1-flash`
89
- if (idx.has(`~${id}`)) return idx.get(`~${id}`)
90
-
91
- // ③ 同一个模型挂在不同厂商前缀下,取最大的那个(真能收的容量)
92
- let best = 0
93
- for (const [k, v] of idx) {
94
- if (k.startsWith("~")) continue
95
- if (k.endsWith(`/${id}`) && v > best) best = v
96
- }
97
- return best || FALLBACK_CONTEXT
98
- }
99
-
100
- export const _internal = { FALLBACK_CONTEXT, loadLibrary }
1
+ // 模型上下文窗口 —— 向 magpie 自己的模型库查
2
+ //
3
+ // ═══ 为什么不能自己填一个值 ═══════════════════════════════════════════
4
+ //
5
+ // magicrew 的两个模型接口**都不返回上下文窗口**:
6
+ // GET /api/v1/flows/models → value / label / vision / tags / configs
7
+ // GET /v1/models → id / object / created_at / owner_by
8
+ //
9
+ // 早前这里一律填 256_000 当保守值,后果是反的:
10
+ // `pi-ai detected context overflow for model "magicrew/deepseek-v4.1-flash"`
11
+ // —— agent 拿 256K 判断"放不下了",而实际窗口是 1M,白砍掉四分之三。
12
+ //
13
+ // magpie 自己有一份模型库(`~/.cache/magpie/models.json`,按厂商分组,
14
+ // 每条带 `limit.context`),它本来就随 magpie 更新。**向它查,不要自己编。**
15
+ //
16
+ // 查不到才退回保守值 —— 宁可保守,不要给出超过真实窗口的数(那会让 agent
17
+ // 以为还有余量,请求超了上游才报错)。
18
+
19
+ import { readFileSync } from "node:fs"
20
+ import { homedir } from "node:os"
21
+ import { join } from "node:path"
22
+
23
+ /**
24
+ * 查不到时的兜底:**1M**。
25
+ *
26
+ * 优先级从高到低:
27
+ * ① 用户在 magpie GUI 里手选的(「上下文窗口」那一行)—— magpie 直接用它,插件的值只是默认
28
+ * ② 本文件从 magpie 模型库查到的真实值(models.dev 数据)
29
+ * ③ 这个兜底
30
+ *
31
+ * ⚠ 兜底故意给大的:给 1M 而实际只有 128K 时,上游会在超限时报错,用户看得到;
32
+ * 给 256K 而实际有 1M 时,agent 会主动拒绝一段本可以处理的对话 —— 那是静默的能力损失,
33
+ * 比报错更难查(`pi-ai detected context overflow` 就是这么来的)。
34
+ */
35
+ const FALLBACK_CONTEXT = 1_048_576
36
+
37
+ let cache = null
38
+ let cacheAt = 0
39
+
40
+ /** models.json 5-6MB,读一次缓存住;10 分钟内不重复读 */
41
+ const CACHE_MS = 10 * 60_000
42
+
43
+ function loadLibrary() {
44
+ if (cache && Date.now() - cacheAt < CACHE_MS) return cache
45
+ const idx = new Map()
46
+ try {
47
+ const raw = readFileSync(join(homedir(), ".cache", "magpie", "models.json"), "utf8")
48
+ const walk = (o) => {
49
+ if (Array.isArray(o)) {
50
+ for (const v of o) walk(v)
51
+ return
52
+ }
53
+ if (!o || typeof o !== "object") return
54
+ const id = typeof o.id === "string" ? o.id : ""
55
+ const ctx = o.limit?.context ?? o.context_length ?? o.context_window
56
+ if (id && Number(ctx) > 0) {
57
+ idx.set(id.toLowerCase(), Number(ctx))
58
+ // 同一个模型可能挂在多个厂商下(deepseek-ai/x、Pro/deepseek-ai/x …),
59
+ // 只保留最大值 —— 那才是它真能收的
60
+ const tail = id.toLowerCase().split("/").pop()
61
+ if (tail) {
62
+ const prev = idx.get(`~${tail}`) || 0
63
+ if (Number(ctx) > prev) idx.set(`~${tail}`, Number(ctx))
64
+ }
65
+ }
66
+ for (const v of Object.values(o)) walk(v)
67
+ }
68
+ walk(JSON.parse(raw))
69
+ } catch {}
70
+ cache = idx
71
+ cacheAt = Date.now()
72
+ return idx
73
+ }
74
+
75
+ /**
76
+ * 这个模型的上下文窗口。
77
+ * @param {string} modelId 插件侧的模型 id,如 `deepseek-v4.1-flash`
78
+ * @returns {number} 字节按 token 计;查不到给保守值
79
+ */
80
+ export function contextOf(modelId) {
81
+ const id = String(modelId || "").toLowerCase()
82
+ if (!id) return FALLBACK_CONTEXT
83
+ const idx = loadLibrary()
84
+
85
+ // ① 全名命中(插件 id 恰好等于库里的 id)
86
+ if (idx.has(id)) return idx.get(id)
87
+
88
+ // ② 尾段命中:库里的 `deepseek-ai/deepseek-v4.1-flash` ↔ 我们的 `deepseek-v4.1-flash`
89
+ if (idx.has(`~${id}`)) return idx.get(`~${id}`)
90
+
91
+ // ③ 同一个模型挂在不同厂商前缀下,取最大的那个(真能收的容量)
92
+ let best = 0
93
+ for (const [k, v] of idx) {
94
+ if (k.startsWith("~")) continue
95
+ if (k.endsWith(`/${id}`) && v > best) best = v
96
+ }
97
+ return best || FALLBACK_CONTEXT
98
+ }
99
+
100
+ export const _internal = { FALLBACK_CONTEXT, loadLibrary }
package/src/tasks.mjs CHANGED
@@ -20,7 +20,7 @@
20
20
  //
21
21
  // 每天跑一次,两步都做,任一步失败不影响另一步。
22
22
 
23
- import { fetchPoints, claimLoginGift, fetchOnboardingTasks, completeOnboardingTask, fetchFirstWorkspaceId } from "./api.mjs"
23
+ import { fetchPoints, claimLoginGift, fetchOnboardingTasks, completeOnboardingTask, fetchFirstWorkspaceId, fetchProjectTopics } from "./api.mjs"
24
24
  import { isAutoChatSettled, noteAutoChat, noteAutoChatFailure } from "./store.mjs"
25
25
 
26
26
  /** 站点 id —— 去重标记按站点分文件存 */
@@ -236,9 +236,9 @@ async function autoFirstWork(h) {
236
236
  /**
237
237
  * 国际站的「首个对话」:没有任务清单可依,只能看它用没用过。
238
238
  *
239
- * 判据是**有没有工作台项目**:项目是发第一条消息时前端顺带建的,
240
- * 一个都没有基本等于从没说过话。已经有项目 = 用过 → 直接记成已补过
241
- * (一次性),以后再也不用查。
239
+ * 判据是**有没有话题**,不是有没有工作台项目 —— 项目在打开工作台时就会
240
+ * 自动建,「建了项目、一句话没说」的新号很常见,而那种号正是需要补一条的。
241
+ * 话题才是「真的发过消息」的证据。
242
242
  *
243
243
  * @returns {{ok?:boolean, settled?:boolean, reason?:string, topicName?:string, model?:string}}
244
244
  */
@@ -254,11 +254,23 @@ async function autoFirstChat(h) {
254
254
  if (!wsId) return { reason: "这个账号还没有工作区,先在网页里进一次" }
255
255
 
256
256
  const { findProjectId, runChatTask } = await import("./chat-task.mjs")
257
- const existing = await findProjectId(h.jwt, h.org, h.host, wsId).catch(() => "")
258
- if (existing) {
259
- // 已有项目 = 早就说过话。直接落一次性标记,省得以后每次登录都查一遍。
260
- noteAutoChat(siteId, h.org, { reason: "已有工作台项目,视为用过" })
261
- return { settled: true, reason: "此前已用过(已有工作台项目)" }
257
+ const pid = await findProjectId(h.jwt, h.org, h.host, wsId).catch(() => "")
258
+ if (pid) {
259
+ let topics
260
+ try {
261
+ topics = await fetchProjectTopics(h.jwt, h.org, pid, h.host)
262
+ } catch (e) {
263
+ // 查不到判据就**这次不发**:宁可漏一轮(下次登录 / 签到还会来),
264
+ // 也不要把「接口抖了一下」当成「没说过话」白花一次积分。
265
+ // 不落标记、不计数 —— 这不是失败,是没拿到判据。
266
+ return { reason: `话题清单查不到(${String(e?.message ?? e)}),这次不发` }
267
+ }
268
+ if (topics.length > 0) {
269
+ // 有话题 = 真的发过消息 → 落一次性标记,以后连查都不查
270
+ noteAutoChat(siteId, h.org, { reason: `已有 ${topics.length} 个话题,视为用过` })
271
+ return { settled: true, reason: `此前已用过(已有 ${topics.length} 个话题)` }
272
+ }
273
+ // 有项目、没话题:项目多半是打开工作台时自动建的,一句话没说 —— 往下补。
262
274
  }
263
275
 
264
276
  const r = await runChatTask({ jwt: h.jwt, org: h.org, host: h.host, workspaceId: wsId })
@@ -307,7 +319,7 @@ export async function ensureFirstTaskOnce(h) {
307
319
  return autoFirstWork(h)
308
320
  }
309
321
 
310
- // 国际站:没有任务清单 → 看有没有工作台项目
322
+ // 国际站:没有任务清单 → 看有没有话题(不是有没有项目)
311
323
  return autoFirstChat(h)
312
324
  }
313
325