dsh-router-core 0.1.0 → 0.3.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.
Files changed (42) hide show
  1. package/README.md +58 -13
  2. package/lib/client-registry.js +824 -153
  3. package/lib/client.js +824 -153
  4. package/lib/index.js +1606 -227
  5. package/lib/types/llm/adapter.d.ts +2 -0
  6. package/lib/types/router/account-pool.d.ts +41 -0
  7. package/lib/types/router/index.d.ts +72 -4
  8. package/lib/types/router/types.d.ts +12 -10
  9. package/lib/types/router/usage-store.d.ts +93 -0
  10. package/lib/types/router/usage-tokens.d.ts +118 -0
  11. package/lib/types/shared.d.ts +69 -0
  12. package/lib/types/supplier-config.d.ts +15 -4
  13. package/lib/types/suppliers/contract.d.ts +70 -21
  14. package/lib/types/suppliers/registry.d.ts +4 -3
  15. package/package.json +2 -1
  16. package/src/client/CombosTab.tsx +28 -16
  17. package/src/client/RouterView.tsx +15 -2
  18. package/src/client/StatsTab.tsx +521 -0
  19. package/src/client/SupplierDetail.tsx +58 -34
  20. package/src/client/router.css +406 -0
  21. package/src/client/workspace-mount.tsx +2 -9
  22. package/src/index.ts +52 -4
  23. package/src/llm/adapter.test.ts +108 -0
  24. package/src/llm/adapter.ts +18 -6
  25. package/src/router/account-pool.test.ts +120 -0
  26. package/src/router/account-pool.ts +161 -0
  27. package/src/router/index.ts +652 -26
  28. package/src/router/models-cache.test.ts +455 -0
  29. package/src/router/precise-dispatch.test.ts +198 -0
  30. package/src/router/types.ts +12 -7
  31. package/src/router/usage-store.ts +499 -0
  32. package/src/router/usage-tokens.ts +327 -0
  33. package/src/router/usage.test.ts +558 -0
  34. package/src/shared.ts +77 -0
  35. package/src/supplier-config.ts +31 -14
  36. package/src/suppliers/contract.ts +71 -8
  37. package/src/suppliers/loader.ts +21 -13
  38. package/src/suppliers/nvidia/plugin.ts +161 -119
  39. package/src/suppliers/opencode/plugin.ts +47 -95
  40. package/src/suppliers/openrouter/plugin.ts +67 -122
  41. package/src/suppliers/registry.test.ts +194 -0
  42. package/src/suppliers/registry.ts +79 -59
@@ -9,8 +9,8 @@
9
9
  * 一个供应商可有多个命名 key,按池顺序/策略尝试。凭证存通用 CredentialStore
10
10
  * (SQLite:auths/credentials.sqlite,{ name, apiKey })。
11
11
  */
12
- import type { ServerResponse } from 'node:http'
13
- import type { ChatRequest, ModelInfo, SupplierStatus } from '../../router/types.ts'
12
+ import type { ChatRequest, ModelInfo } from '../../router/types.ts'
13
+ import type { AccountState, ChatOnceResult, SupplierStatusNow } from '../contract.ts'
14
14
  import type { SupplierEnv, SupplierModule } from '../contract.ts'
15
15
 
16
16
  export const id = 'openrouter'
@@ -22,7 +22,7 @@ export const icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAA
22
22
  const BASE = 'https://openrouter.ai/api/v1'
23
23
  const MODELS_URL = `${BASE}/models`
24
24
  const CHAT_URL = `${BASE}/chat/completions`
25
- const COOL_DOWN_MS = 60 * 1000 // 失败冷却 60s
25
+ /** 默认前缀(用户可在面板改;loader 包装会优先用 store 里的值)。 */
26
26
 
27
27
  /** 免费模型过滤(同 9Router openrouter-free):pricing 全 0 且 context ≥ 200k。 */
28
28
  function isFreeModelMeta(m: { pricing?: { prompt?: string; completion?: string }; context_length?: number }): boolean {
@@ -33,10 +33,11 @@ function isFreeModelMeta(m: { pricing?: { prompt?: string; completion?: string }
33
33
  )
34
34
  }
35
35
 
36
+
36
37
  /** 剥 alias 前缀(or/xxx → xxx)。 */
37
- function stripAlias(model: string): string {
38
- const slash = model.lastIndexOf('/')
39
- return slash >= 0 ? model.slice(slash + 1) : model
38
+ /** 剥本供应商 alias 前缀(只剥自己的,模型 id 自带的斜杠保留)。 */
39
+ function stripAlias(model: string, alias: string): string {
40
+ return alias !== '' && model.startsWith(`${alias}/`) ? model.slice(alias.length + 1) : model
40
41
  }
41
42
 
42
43
  interface ApiKeyAccount {
@@ -48,9 +49,8 @@ export default function factory(env: SupplierEnv): SupplierModule {
48
49
  // 模型缓存由 dsh-router 核心统一管;插件每次拉取,失败回退上次成功结果
49
50
  let modelsCache: ModelInfo[] | undefined
50
51
  let freeIds = new Set<string>()
51
- /** 失败冷却:uid → until(ms)。 */
52
- const cooling = new Map<string, number>()
53
-
52
+ /** 上次 chatOnce 失败原因(供核心测试模型汇总诊断)。 */
53
+ let lastErr: string | undefined
54
54
  function listKeys(): string[] {
55
55
  return env.credentials.list(id)
56
56
  }
@@ -67,8 +67,9 @@ export default function factory(env: SupplierEnv): SupplierModule {
67
67
  return uids.map((uid) => ({ uid, acct: getKey(uid)! })).filter((x) => x.acct !== undefined)
68
68
  }
69
69
 
70
- function isCooling(uid: string, now = Date.now()): boolean {
71
- return (cooling.get(uid) ?? 0) > now
70
+ /** 当前前缀(与 loader 包装一致:store 覆盖默认值)。 */
71
+ function currentAlias(): string {
72
+ return env.store.get(id).alias || id
72
73
  }
73
74
 
74
75
  async function refreshModels(): Promise<ModelInfo[]> {
@@ -109,20 +110,18 @@ export default function factory(env: SupplierEnv): SupplierModule {
109
110
  name,
110
111
  priority,
111
112
  icon,
112
- status: (): SupplierStatus => {
113
- const now = Date.now()
113
+ status: (): SupplierStatusNow => {
114
+ // 只报「现在状态」:凭证是否存在。冷却/禁用/错误累计由核心叠加。
114
115
  const accounts = orderedKeys().map(({ uid, acct }) => ({
115
116
  uid,
116
117
  nickname: acct.name || 'API Key',
117
118
  credits: 0,
118
- cooling: isCooling(uid, now),
119
- disabled: false,
120
- err_count: 0,
119
+ state: 'ok' as AccountState,
121
120
  }))
122
121
  return { id, name, accounts }
123
122
  },
124
123
  listModels: (force?: boolean): Promise<ModelInfo[]> => allModels(!!force),
125
- getAlias: (): string => 'or',
124
+ getAlias: (): string => id,
126
125
  async addApiKey(input: { name: string; apiKey: string }): Promise<{ ok: boolean; error?: string; account?: { uid: string; nickname: string } }> {
127
126
  const key = input.apiKey.trim()
128
127
  if (key === '') return { ok: false, error: 'API key 不能为空' }
@@ -150,43 +149,21 @@ export default function factory(env: SupplierEnv): SupplierModule {
150
149
  async removeLink(uid: string): Promise<boolean> {
151
150
  if (getKey(uid) === undefined) return false
152
151
  env.credentials.remove(id, uid)
153
- cooling.delete(uid)
154
152
  return true
155
153
  },
156
- async testModel(mid: string): Promise<{ ok: boolean; error?: string }> {
157
- const base = stripAlias(mid)
158
- const keys = orderedKeys()
159
- if (keys.length === 0) return { ok: false, error: '未添加 API key(点「添加链接」配置)' }
154
+ lastError: (): string | undefined => lastErr,
155
+ /** 对单个 key 调一次上游。选号/冷却/换号是核心的活,这里只报结果。 */
156
+ async chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult> {
157
+ const base = stripAlias(req.model, currentAlias())
160
158
  if (!freeIds.has(base) && !(await allModels(false)).some((m) => m.id === base)) {
161
- return { ok: false, error: `unknown free model ${JSON.stringify(mid)}` }
159
+ lastErr = `unknown free model ${JSON.stringify(req.model)}`
160
+ return { ok: false, state: 'no_such_model', message: lastErr }
162
161
  }
163
- for (const { acct } of keys) {
164
- try {
165
- const resp = await fetch(CHAT_URL, {
166
- method: 'POST',
167
- headers: {
168
- 'Content-Type': 'application/json',
169
- Authorization: `Bearer ${acct.apiKey}`,
170
- 'HTTP-Referer': 'https://endpoint-proxy.local',
171
- 'X-Title': 'dsh-router',
172
- },
173
- body: JSON.stringify({ model: base, messages: [{ role: 'user', content: 'ping' }], stream: false, max_tokens: 1 }),
174
- signal: AbortSignal.timeout(30000),
175
- })
176
- if (resp.ok) return { ok: true }
177
- const body = await resp.text().catch(() => '')
178
- return { ok: false, error: `upstream ${resp.status}: ${body.slice(0, 200)}` }
179
- } catch (err) {
180
- return { ok: false, error: (err as Error).message }
181
- }
162
+ const acct = getKey(uid)
163
+ if (acct === undefined) {
164
+ lastErr = `unknown account ${JSON.stringify(uid)}`
165
+ return { ok: false, state: 'no_such_model', message: lastErr }
182
166
  }
183
- return { ok: false, error: 'no api key' }
184
- },
185
- async chatCompletions(req: ChatRequest, res: ServerResponse): Promise<boolean> {
186
- const base = stripAlias(req.model)
187
- if (!freeIds.has(base) && !(await allModels(false)).some((m) => m.id === base)) return false
188
- const keys = orderedKeys().filter(({ uid }) => !isCooling(uid))
189
- if (keys.length === 0) return false // 无健康 key → 路由器 fallback
190
167
 
191
168
  let body = req.rawBody
192
169
  try {
@@ -197,74 +174,50 @@ export default function factory(env: SupplierEnv): SupplierModule {
197
174
  // 保持原样
198
175
  }
199
176
 
200
- let lastErr = ''
201
- for (const { uid, acct } of keys) {
202
- let upstream: Response
203
- try {
204
- upstream = await fetch(CHAT_URL, {
205
- method: 'POST',
206
- headers: {
207
- 'Content-Type': 'application/json',
208
- Authorization: `Bearer ${acct.apiKey}`,
209
- 'HTTP-Referer': 'https://endpoint-proxy.local',
210
- 'X-Title': 'dsh-router',
211
- },
212
- body,
213
- signal: AbortSignal.timeout(120000),
214
- })
215
- } catch (err) {
216
- cooling.set(uid, Date.now() + COOL_DOWN_MS)
217
- lastErr = (err as Error).message
218
- continue
219
- }
220
- // 非 2xx:冷却该 key,试下一个
221
- if (upstream.status < 200 || upstream.status >= 300) {
222
- cooling.set(uid, Date.now() + COOL_DOWN_MS)
223
- const text = await upstream.text().catch(() => '')
224
- lastErr = `upstream ${upstream.status}: ${text.slice(0, 120)}`
225
- continue
226
- }
227
- // 成功:透传(流式 SSE 或 JSON)
228
- if (req.stream) {
229
- res.writeHead(upstream.status, {
230
- 'Content-Type': 'text/event-stream',
231
- 'Cache-Control': 'no-cache',
232
- Connection: 'keep-alive',
233
- 'X-Accel-Buffering': 'no',
234
- })
235
- if (!upstream.body) {
236
- res.end()
237
- return true
238
- }
239
- const reader = upstream.body.getReader()
240
- try {
241
- for (;;) {
242
- const { done, value } = await reader.read()
243
- if (done) break
244
- res.write(Buffer.from(value))
245
- if (typeof (res as { flushHeaders?: () => void }).flushHeaders === 'function') {
246
- ;(res as { flushHeaders: () => void }).flushHeaders()
247
- }
248
- }
249
- } finally {
250
- reader.releaseLock()
251
- }
252
- res.end()
253
- return true
254
- }
177
+ let upstream: Response
178
+ try {
179
+ upstream = await fetch(CHAT_URL, {
180
+ method: 'POST',
181
+ headers: {
182
+ 'Content-Type': 'application/json',
183
+ Authorization: `Bearer ${acct.apiKey}`,
184
+ 'HTTP-Referer': 'https://endpoint-proxy.local',
185
+ 'X-Title': 'dsh-router',
186
+ },
187
+ body,
188
+ signal: AbortSignal.timeout(120000),
189
+ })
190
+ } catch (err) {
191
+ lastErr = (err as Error).message
192
+ return { ok: false, state: 'transport', message: lastErr }
193
+ }
194
+
195
+ if (upstream.status < 200 || upstream.status >= 300) {
255
196
  const text = await upstream.text().catch(() => '')
256
- let status = upstream.status
257
- if (status === 200 && text === '') status = 502
258
- try {
259
- writeJson(res, status, JSON.parse(text))
260
- } catch {
261
- writeJson(res, status, { error: { message: text.slice(0, 500), type: 'api_error', code: 'upstream_error' } })
197
+ lastErr = `upstream ${upstream.status}: ${text.slice(0, 120)}`
198
+ // 401/403 = 这个 key 本身不可用;429 = 限流;其余归为说不清
199
+ const state: AccountState =
200
+ upstream.status === 429 ? 'rate_limit'
201
+ : upstream.status === 401 || upstream.status === 403 ? 'session_dead'
202
+ : upstream.status === 404 ? 'unavailable'
203
+ : 'unknown'
204
+ return { ok: false, state, message: lastErr }
205
+ }
206
+
207
+ // 流式:上游已是 OpenAI SSE,原样交回核心写
208
+ if (req.stream) {
209
+ if (!upstream.body) {
210
+ lastErr = 'openrouter upstream: empty stream body'
211
+ return { ok: false, state: 'transport', message: lastErr }
262
212
  }
263
- return true
213
+ return { ok: true, stream: upstream.body }
264
214
  }
265
- // 所有 key 都失败:记录日志,返回 false 让路由器 fallback
266
- env.log(`openrouter chat failed: ${lastErr}`)
267
- return false
215
+ const text = await upstream.text().catch(() => '')
216
+ if (text === '') {
217
+ lastErr = 'openrouter upstream: empty body'
218
+ return { ok: false, state: 'transport', message: lastErr }
219
+ }
220
+ return { ok: true, status: upstream.status, body: text }
268
221
  },
269
222
  dispose: (): void => {
270
223
  modelsCache = undefined
@@ -272,11 +225,3 @@ export default function factory(env: SupplierEnv): SupplierModule {
272
225
  }
273
226
  }
274
227
 
275
- function writeJson(res: ServerResponse, status: number, body: unknown): void {
276
- const payload = typeof body === 'string' ? body : JSON.stringify(body)
277
- res.writeHead(status, {
278
- 'Content-Type': 'application/json; charset=utf-8',
279
- 'Cache-Control': 'no-store',
280
- })
281
- res.end(payload)
282
- }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * 签到汇聚(POST /suppliers/:id/checkin)契约测试 —— node --test 直接跑(Node 24 原生剥 TS 类型)。
3
+ *
4
+ * 锁定的契约:checkinNow 是**单账号**能力,遍历所有链接 + 汇总是核心的活:
5
+ * - 禁用链接不签(不动它)
6
+ * - 某个链接抛错 → 记成 error,不带垮其它链接
7
+ * - succeeded / already 按 status 统计,ok = 有任一成功或今日已签
8
+ */
9
+ import test from 'node:test'
10
+ import assert from 'node:assert/strict'
11
+ import { supplierRoutes } from './registry.ts'
12
+ import type { SupplierConfigStore } from '../supplier-config.ts'
13
+
14
+ type CheckinResult = { uid: string; ok: boolean; status: string; message?: string }
15
+
16
+ /** 最小假 res:只接住 writeJson 写出的状态码与 JSON body。 */
17
+ function fakeRes(): { status: number; body: unknown; res: never } {
18
+ const out = { status: 0, body: undefined as unknown }
19
+ const res = {
20
+ writeHead(status: number): void {
21
+ out.status = status
22
+ },
23
+ end(payload: string): void {
24
+ out.body = JSON.parse(payload)
25
+ },
26
+ }
27
+ return { get status() { return out.status }, get body() { return out.body }, res: res as never }
28
+ }
29
+
30
+ type FakeAccount = { uid: string; disabled?: boolean; credits?: number }
31
+
32
+ /** 造一个只带 checkinNow 的假供应商:status() 返回账号列表,checkinNow 按 uid 应答。
33
+ * accounts 可传函数(每次 status() 重新求值,用来模拟插件后台异步刷积分)。 */
34
+ function fixture(
35
+ accounts: FakeAccount[] | (() => FakeAccount[]),
36
+ checkinNow: (uid: string) => Promise<{ ok: boolean; status: string; message?: string }>,
37
+ ): never {
38
+ const list = (): FakeAccount[] => (typeof accounts === 'function' ? accounts() : accounts)
39
+ const supplier = {
40
+ id: 'fake',
41
+ name: 'fake',
42
+ status: () => ({
43
+ id: 'fake',
44
+ name: 'fake',
45
+ accounts: list().map((a) => ({
46
+ uid: a.uid,
47
+ nickname: a.uid,
48
+ credits: a.credits ?? 0,
49
+ cooling: false,
50
+ disabled: a.disabled ?? false,
51
+ err_count: 0,
52
+ })),
53
+ }),
54
+ __module: { checkinNow },
55
+ }
56
+ return { supplier, capabilities: new Set(['checkinNow']), source: 'builtin' } as never
57
+ }
58
+
59
+ /** 打到某条路由上,返回 { status, body }。B 是期望的响应体形状(让 tsc 真检查)。 */
60
+ async function post<B>(loaded: never, path: string): Promise<{ status: number; body: B }> {
61
+ const routes = supplierRoutes('/router/api', loaded, {} as SupplierConfigStore, {} as never)
62
+ const route = routes.find((r) => r.path === `/router/api/suppliers/fake/${path}`)
63
+ assert.ok(route, `${path} 路由应注册`)
64
+ const sink = fakeRes() // getter 延迟读,不能解构
65
+ await route!.handler({} as never, sink.res)
66
+ return { status: sink.status, body: sink.body as B }
67
+ }
68
+
69
+ /** 跑到 /checkin 路由上,返回 { status, body }。 */
70
+ async function postCheckin(loaded: never): Promise<{ status: number; body: CheckinPayload }> {
71
+ return await post<CheckinPayload>(loaded, 'checkin')
72
+ }
73
+
74
+ /** POST /links/refresh 的响应体。 */
75
+ interface RefreshPayload {
76
+ ok: boolean
77
+ changed: boolean
78
+ accounts: Array<{ uid: string; credits: number }>
79
+ error?: string
80
+ }
81
+
82
+ interface CheckinPayload {
83
+ ok: boolean
84
+ total: number
85
+ succeeded: number
86
+ already: number
87
+ results?: CheckinResult[]
88
+ error?: string
89
+ }
90
+
91
+ test('签到:遍历所有链接,逐个调 checkinNow(uid)', async () => {
92
+ const seen: string[] = []
93
+ const { status, body } = await postCheckin(
94
+ fixture([{ uid: 'a' }, { uid: 'b' }], async (uid) => {
95
+ seen.push(uid)
96
+ return { ok: true, status: 'ok', message: '+100 积分' }
97
+ }),
98
+ )
99
+ assert.deepEqual(seen, ['a', 'b'])
100
+ assert.equal(status, 200)
101
+ assert.deepEqual(
102
+ { ok: body.ok, total: body.total, succeeded: body.succeeded, already: body.already },
103
+ { ok: true, total: 2, succeeded: 2, already: 0 },
104
+ )
105
+ assert.deepEqual(body.results?.map((r) => r.uid), ['a', 'b'])
106
+ })
107
+
108
+ test('签到:禁用链接照样签(禁用由插件自己判定,核心不筛)', async () => {
109
+ const seen: string[] = []
110
+ const { body } = await postCheckin(
111
+ fixture([{ uid: 'a', disabled: true }, { uid: 'b' }], async (uid) => {
112
+ seen.push(uid)
113
+ // 插件(如 traework)自己会对禁用链接返回 status:'disabled'
114
+ return uid === 'a' ? { ok: false, status: 'disabled', message: '链接已禁用' } : { ok: true, status: 'ok' }
115
+ }),
116
+ )
117
+ assert.deepEqual(seen, ['a', 'b'])
118
+ assert.equal(body.total, 2)
119
+ assert.equal(body.succeeded, 1)
120
+ assert.equal(body.results?.[0]?.status, 'disabled')
121
+ })
122
+
123
+ test('签到:单链接抛错记成 error,其余照签', async () => {
124
+ const { status, body } = await postCheckin(
125
+ fixture([{ uid: 'a' }, { uid: 'b' }], async (uid) => {
126
+ if (uid === 'a') throw new Error('boom')
127
+ return { ok: true, status: 'ok' }
128
+ }),
129
+ )
130
+ assert.equal(status, 200)
131
+ assert.equal(body.succeeded, 1)
132
+ assert.equal(body.results?.[0]?.status, 'error')
133
+ assert.equal(body.results?.[0]?.message, 'boom')
134
+ assert.equal(body.results?.[1]?.status, 'ok')
135
+ })
136
+
137
+ test('签到:今日已签(already)算成功,不计入 succeeded', async () => {
138
+ const { status, body } = await postCheckin(
139
+ fixture([{ uid: 'a' }], async () => ({ ok: true, status: 'already', message: '今日已签到' })),
140
+ )
141
+ assert.equal(status, 200)
142
+ assert.deepEqual({ ok: body.ok, succeeded: body.succeeded, already: body.already }, { ok: true, succeeded: 0, already: 1 })
143
+ })
144
+
145
+ test('签到:全失败 → 400;无链接 → total 0 且 ok false', async () => {
146
+ const fail = await postCheckin(fixture([{ uid: 'a' }], async () => ({ ok: false, status: 'error', message: '凭证失效 401' })))
147
+ assert.equal(fail.status, 400)
148
+ assert.equal(fail.body.ok, false)
149
+ const empty = await postCheckin(fixture([], async () => ({ ok: true, status: 'ok' })))
150
+ assert.equal(empty.status, 400)
151
+ assert.equal(empty.body.total, 0)
152
+ })
153
+
154
+ test('刷新:等后台积分落地后返回最新快照(changed=true)', async () => {
155
+ // 模拟插件 fire-and-forget 刷积分:第 2 次 status() 起才带上新值
156
+ let ticks = 0
157
+ const { status, body } = await post<RefreshPayload>(
158
+ fixture(() => [{ uid: 'a', credits: ticks++ >= 2 ? 120 : 0 }], async () => ({ ok: true, status: 'ok' })),
159
+ 'links/refresh',
160
+ )
161
+ assert.equal(status, 200)
162
+ assert.equal(body.ok, true)
163
+ assert.equal(body.changed, true)
164
+ assert.equal(body.accounts[0]?.credits, 120)
165
+ })
166
+
167
+ test('刷新:快照稳定就早退,不死等超时', async () => {
168
+ const started = Date.now()
169
+ const { status, body } = await post<RefreshPayload>(
170
+ fixture([{ uid: 'a', credits: 7 }], async () => ({ ok: true, status: 'ok' })),
171
+ 'links/refresh',
172
+ )
173
+ assert.equal(status, 200)
174
+ assert.equal(body.changed, false)
175
+ assert.equal(body.accounts[0]?.credits, 7)
176
+ // settleStatus 稳定 2 次即 break(~600ms),远小于 3s 超时
177
+ assert.ok(Date.now() - started < 2000, `刷新不该等满超时,实际 ${Date.now() - started}ms`)
178
+ })
179
+
180
+ test('刷新:status() 抛错 → 500 带错误信息', async () => {
181
+ const supplier = {
182
+ id: 'fake',
183
+ name: 'fake',
184
+ status: (): never => { throw new Error('status boom') },
185
+ __module: { checkinNow: async () => ({ ok: true, status: 'ok' }) },
186
+ }
187
+ const { status, body } = await post<RefreshPayload>(
188
+ { supplier, capabilities: new Set(['checkinNow']), source: 'builtin' } as never,
189
+ 'links/refresh',
190
+ )
191
+ assert.equal(status, 500)
192
+ assert.equal(body.ok, false)
193
+ assert.match(body.error ?? '', /status boom/)
194
+ })