@mazjq/zhiro 0.1.2 → 0.2.1

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.
@@ -8,25 +8,25 @@ import {
8
8
  nodeArgs,
9
9
  noteModelRefused,
10
10
  upsertFact
11
- } from "./chunk-UMCKC7XP.js";
11
+ } from "./chunk-VOUILWMI.js";
12
12
  import {
13
13
  callingAgent,
14
14
  delegateRun,
15
15
  isExternal
16
- } from "./chunk-F6DLONN7.js";
16
+ } from "./chunk-7IV2NG2D.js";
17
17
  import {
18
18
  DomainError,
19
19
  NotFound
20
- } from "./chunk-LH25QWMP.js";
20
+ } from "./chunk-ZKD2BKN3.js";
21
21
  import {
22
22
  parseBaseUrl,
23
23
  parseCsv,
24
24
  readBase
25
- } from "./chunk-YYNUPNBH.js";
25
+ } from "./chunk-4UWMDKMH.js";
26
26
  import {
27
27
  now,
28
28
  tx
29
- } from "./chunk-5KCN32HQ.js";
29
+ } from "./chunk-4EPNDXRS.js";
30
30
  import {
31
31
  __name
32
32
  } from "./chunk-2SSYR7VW.js";
@@ -1256,7 +1256,12 @@ function brief(kind, d) {
1256
1256
  }
1257
1257
  }
1258
1258
  __name(brief, "brief");
1259
- var toRow = /* @__PURE__ */ __name((r) => ({ kind: r.kind, data: JSON.parse(r.data_json), source: r.source, fetchedAt: r.fetched_at, ageDays: Math.floor((Date.now() - Date.parse(r.fetched_at)) / 864e5) }), "toRow");
1259
+ var EVERY_DAYS = { listing: 180, basic: 90, sectors: 90, finance: 30, dividends: 30, holders: 30, forecast: 7, reports: 7, irm: 7, announcements: 1, news: 1, ipo: 3, sme: 365 };
1260
+ var everyDays = /* @__PURE__ */ __name((kind, data) => kind === "listing" && data?.none ? 30 : kind === "ipo" && !data?.szse ? 30 : EVERY_DAYS[kind] ?? 30, "everyDays");
1261
+ var toRow = /* @__PURE__ */ __name((r, now3 = Date.now()) => {
1262
+ const data = JSON.parse(r.data_json), age = (now3 - Date.parse(r.fetched_at)) / 864e5, every = everyDays(r.kind, data);
1263
+ return { kind: r.kind, data, source: r.source, fetchedAt: r.fetched_at, ageDays: Math.floor(age), everyDays: every, stale: age > every };
1264
+ }, "toRow");
1260
1265
  function getIntelDetail(s, companyId, kind) {
1261
1266
  const r = s.domain.db.prepare("select * from company_intel where company_id = ? and kind = ?").get(companyId, kind);
1262
1267
  if (!r) throw new NotFound(`企业情况 ${companyId} ${kind}`);
@@ -1271,97 +1276,110 @@ function getIntel(s, companyId) {
1271
1276
  return JSON.stringify(b) === JSON.stringify(row2.data) ? row2 : { ...row2, data: b, more: true };
1272
1277
  });
1273
1278
  const sme = smeMatch(s.domain.db, co.name);
1274
- const listing = rows.find((r) => r.kind === "listing")?.data ?? null;
1275
- return { companyId, companyName: co.name, listing, rows: rows.filter((r) => r.kind !== "listing"), sme, stale: rows.length === 0 || rows.some((r) => r.ageDays > 30) };
1279
+ const listingRow = rows.find((r) => r.kind === "listing");
1280
+ const staleKinds = rows.length === 0 ? ["listing"] : rows.filter((r) => r.stale).map((r) => r.kind);
1281
+ return { companyId, companyName: co.name, listing: listingRow?.data ?? null, rows: rows.filter((r) => r.kind !== "listing"), sme, stale: staleKinds.length > 0, staleKinds, refreshing: running2.has(companyId) };
1276
1282
  }
1277
1283
  __name(getIntel, "getIntel");
1284
+ function readIntel(s, companyId) {
1285
+ const card = getIntel(s, companyId);
1286
+ const kept = card.listing !== null;
1287
+ if (kept && card.stale && !card.refreshing) void refreshIntel(s, companyId).catch(() => {
1288
+ });
1289
+ return { ...card, refreshing: card.refreshing || kept && card.stale };
1290
+ }
1291
+ __name(readIntel, "readIntel");
1278
1292
  var put = /* @__PURE__ */ __name((s, companyId, kind, data, source) => s.domain.db.prepare("insert or replace into company_intel(company_id, kind, data_json, source, fetched_at) values (?,?,?,?,?)").run(companyId, kind, JSON.stringify(data), source, (/* @__PURE__ */ new Date()).toISOString()), "put");
1279
- async function refreshIntel(s, companyId) {
1293
+ var running2 = /* @__PURE__ */ new Map();
1294
+ var failed = /* @__PURE__ */ new Map();
1295
+ var mayTry = /* @__PURE__ */ __name((key, now3) => (failed.get(key)?.until ?? 0) <= now3, "mayTry");
1296
+ var noteFailure = /* @__PURE__ */ __name((key, now3) => {
1297
+ const n = (failed.get(key)?.n ?? 0) + 1;
1298
+ failed.set(key, { n, until: now3 + Math.min(24, 2 ** (n - 1)) * 36e5 });
1299
+ }, "noteFailure");
1300
+ function refreshIntel(s, companyId, opts = {}) {
1301
+ const on = running2.get(companyId);
1302
+ if (on) return on;
1303
+ const job = refreshKinds(s, companyId, !!opts.force).finally(() => running2.delete(companyId));
1304
+ running2.set(companyId, job);
1305
+ return job;
1306
+ }
1307
+ __name(refreshIntel, "refreshIntel");
1308
+ async function refreshKinds(s, companyId, force) {
1280
1309
  const co = s.domain.db.prepare("select name from company where id = ?").get(companyId);
1281
1310
  if (!co) throw new NotFound(`company ${companyId}`);
1311
+ const now3 = Date.now();
1312
+ const have = new Map(s.domain.db.prepare("select * from company_intel where company_id = ?").all(companyId).map((r) => [r.kind, toRow(r, now3)]));
1282
1313
  const errors = {};
1283
- let listing = await resolveListing(co.name).catch((e) => {
1284
- errors.listing = String(e?.message ?? e);
1285
- return null;
1286
- });
1287
- let via = "cninfo:topSearch";
1288
- if (!listing) {
1289
- await sleep(300);
1290
- listing = await resolveNeeq(co.name, normName(co.name)).catch((e) => {
1291
- errors.listing = String(e?.message ?? e);
1292
- return null;
1293
- });
1294
- via = "eastmoney:codetable";
1295
- }
1296
- if (!listing) {
1314
+ const due = /* @__PURE__ */ __name((kind) => force || !have.has(kind) || have.get(kind).stale, "due");
1315
+ const attempt = /* @__PURE__ */ __name(async (kind, fn, source) => {
1316
+ const key = `${companyId}:${kind}`;
1317
+ if (!force && !mayTry(key, now3)) return;
1318
+ try {
1319
+ put(s, companyId, kind, await fn(), source);
1320
+ failed.delete(key);
1321
+ } catch (e) {
1322
+ errors[kind] = String(e?.message ?? e).slice(0, 200);
1323
+ noteFailure(key, now3);
1324
+ }
1297
1325
  await sleep(300);
1298
- listing = await resolveUsListing(co.name).catch((e) => {
1326
+ }, "attempt");
1327
+ let listing = have.get("listing")?.data?.none ? null : have.get("listing")?.data ?? null;
1328
+ let changed = false;
1329
+ if (due("listing")) {
1330
+ const was = have.get("listing")?.data;
1331
+ let found = await resolveListing(co.name).catch((e) => {
1299
1332
  errors.listing = String(e?.message ?? e);
1300
1333
  return null;
1301
1334
  });
1302
- via = "eastmoney:search";
1303
- }
1304
- put(s, companyId, "listing", listing ?? { none: true }, listing ? via : "cninfo:topSearch+eastmoney:codetable+eastmoney:search");
1305
- if (listing && listing.market === "美股") {
1306
- for (const [kind, fn, source] of [["basic", () => fetchUsBasic(listing.code), "eastmoney:USF10"], ["finance", () => fetchUsFinance(listing.code), "eastmoney:USF10"]]) {
1307
- try {
1308
- put(s, companyId, kind, await fn(), source);
1309
- } catch (e) {
1310
- errors[kind] = String(e?.message ?? e).slice(0, 200);
1311
- }
1335
+ let via = "cninfo:topSearch";
1336
+ if (!found) {
1312
1337
  await sleep(300);
1338
+ found = await resolveNeeq(co.name, normName(co.name)).catch((e) => {
1339
+ errors.listing = String(e?.message ?? e);
1340
+ return null;
1341
+ });
1342
+ via = "eastmoney:codetable";
1313
1343
  }
1314
- }
1315
- if (listing && listing.market === "新三板") {
1316
- for (const [kind, fn, source] of [["basic", () => fetchNeeqBasic(listing.code), "neeq:detailCompany+年报"], ["finance", () => fetchNeeqFinance(listing.code), "eastmoney:xinsanban"]]) {
1317
- try {
1318
- put(s, companyId, kind, await fn(), source);
1319
- } catch (e) {
1320
- errors[kind] = String(e?.message ?? e).slice(0, 200);
1321
- }
1344
+ if (!found) {
1322
1345
  await sleep(300);
1346
+ found = await resolveUsListing(co.name).catch((e) => {
1347
+ errors.listing = String(e?.message ?? e);
1348
+ return null;
1349
+ });
1350
+ via = "eastmoney:search";
1323
1351
  }
1324
- }
1325
- if (listing && listing.market === "港股") {
1326
- try {
1327
- put(s, companyId, "finance", await fetchHkFinance(listing.code), "eastmoney:HKF10");
1328
- } catch (e) {
1329
- errors.finance = String(e?.message ?? e).slice(0, 200);
1352
+ if (found || !errors.listing) put(s, companyId, "listing", found ?? { none: true }, found ? via : "cninfo:topSearch+eastmoney:codetable+eastmoney:search");
1353
+ if (found || !errors.listing) {
1354
+ changed = !was || (was.none ? !!found : !found || was.code !== found.code || was.market !== found.market);
1355
+ listing = found;
1330
1356
  }
1331
- await sleep(300);
1332
1357
  }
1333
- if (listing && listing.market === "A股") {
1334
- const steps = [
1335
- ["basic", () => fetchBasic(listing.code), "eastmoney:F10"],
1336
- ["finance", () => fetchFinance(listing.code), "eastmoney:ZYZB+sina:lrb"],
1337
- ["dividends", () => fetchDividends(listing.code), "eastmoney:datacenter"],
1338
- ["announcements", () => fetchAnnouncements(listing.code, listing.orgId), "cninfo:hisAnnouncement"],
1339
- ["holders", () => fetchHolders(listing.code), "eastmoney:F10-shareholders"],
1340
- ["irm", () => fetchIrm(listing.code), "cninfo:irm"],
1341
- ["sectors", () => fetchSectors(listing.code), "eastmoney:slist"],
1342
- ["reports", () => fetchReports(listing.code), "eastmoney:reportapi"],
1343
- ["forecast", () => fetchForecast(listing.code), "eastmoney:datacenter"]
1344
- ];
1345
- for (const [kind, fn, source] of steps) {
1346
- try {
1347
- put(s, companyId, kind, await fn(), source);
1348
- } catch (e) {
1349
- errors[kind] = String(e?.message ?? e).slice(0, 200);
1350
- }
1351
- await sleep(300);
1352
- }
1353
- }
1354
- for (const [kind, fn, source] of [["news", () => fetchNews(co.name), "wallstreetcn:search"], ["ipo", () => fetchIpo(co.name), "szse:projectrends"]]) {
1355
- try {
1356
- put(s, companyId, kind, await fn(), source);
1357
- } catch (e) {
1358
- errors[kind] = String(e?.message ?? e).slice(0, 200);
1359
- }
1360
- await sleep(300);
1358
+ const go = /* @__PURE__ */ __name((kind) => changed || due(kind), "go");
1359
+ const steps = [];
1360
+ if (listing?.market === "美股") steps.push(["basic", () => fetchUsBasic(listing.code), "eastmoney:USF10"], ["finance", () => fetchUsFinance(listing.code), "eastmoney:USF10"]);
1361
+ if (listing?.market === "新三板") steps.push(["basic", () => fetchNeeqBasic(listing.code), "neeq:detailCompany+年报"], ["finance", () => fetchNeeqFinance(listing.code), "eastmoney:xinsanban"]);
1362
+ if (listing?.market === "港股") steps.push(["finance", () => fetchHkFinance(listing.code), "eastmoney:HKF10"]);
1363
+ if (listing?.market === "A股") {
1364
+ const l = listing;
1365
+ steps.push(
1366
+ ["basic", () => fetchBasic(l.code), "eastmoney:F10"],
1367
+ ["finance", () => fetchFinance(l.code), "eastmoney:ZYZB+sina:lrb"],
1368
+ ["dividends", () => fetchDividends(l.code), "eastmoney:datacenter"],
1369
+ ["announcements", () => fetchAnnouncements(l.code, l.orgId), "cninfo:hisAnnouncement"],
1370
+ ["holders", () => fetchHolders(l.code), "eastmoney:F10-shareholders"],
1371
+ ["irm", () => fetchIrm(l.code), "cninfo:irm"],
1372
+ ["sectors", () => fetchSectors(l.code), "eastmoney:slist"],
1373
+ ["reports", () => fetchReports(l.code), "eastmoney:reportapi"],
1374
+ ["forecast", () => fetchForecast(l.code), "eastmoney:datacenter"]
1375
+ );
1361
1376
  }
1377
+ steps.push(["news", () => fetchNews(co.name), "wallstreetcn:search"]);
1378
+ if (listing?.market !== "A股") steps.push(["ipo", () => fetchIpo(co.name), "szse:projectrends"]);
1379
+ for (const [kind, fn, source] of steps) if (go(kind)) await attempt(kind, fn, source);
1362
1380
  return { ...getIntel(s, companyId), errors };
1363
1381
  }
1364
- __name(refreshIntel, "refreshIntel");
1382
+ __name(refreshKinds, "refreshKinds");
1365
1383
 
1366
1384
  // apps/daemon/src/service/industry.ts
1367
1385
  var UA3 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/128.0 Safari/537.36";
@@ -2300,6 +2318,7 @@ var tools = {
2300
2318
  degree: Type.Optional(Type.String({ description: "学历要求含任一词" })),
2301
2319
  city: Type.Optional(Type.String()),
2302
2320
  watchedOnly: Type.Optional(Type.Boolean()),
2321
+ internship: Type.Optional(Type.Boolean({ description: "只列实习岗:岗位名含「实习」/ intern,或来源把这批标成实习" })),
2303
2322
  includeExpired: Type.Optional(Type.Boolean({ description: "默认只列截止日未过的岗位;true 连已截止的也列" })),
2304
2323
  limit: Type.Optional(Type.Number({ default: 100 })),
2305
2324
  offset: Type.Optional(Type.Number({ default: 0, description: "跳过前几条(翻页);简版结果的 hasMore 说还有没有" })),
@@ -2596,7 +2615,7 @@ var tools = {
2596
2615
  "research.company": defineTool({
2597
2616
  name: "research.company",
2598
2617
  description: "派发一次公司调研:由系统侧 Claude Code worker(有联网搜索)在隔离 run 目录中检索官网/招聘系统/就业网/公众号/牛客,产出带证据等级的中文报告,落 vault/research/。异步,返回 runId;用 research.get 查看。",
2599
- input: Type.Object({ companyName: Type.String(), question: Type.Optional(Type.String({ description: "具体想知道什么;默认 2027 届校招全貌" })) }),
2618
+ input: Type.Object({ companyName: Type.String(), question: Type.Optional(Type.String({ description: "具体想知道什么;默认 2027 届校招全貌。「企业情况」= 经营情况;「就业评估」= 就业评估报告(D-29:职路先放同行百分位和历年财务,再由调研员写利润为什么变、钱去了哪、激励是否覆盖技术人员、上市进展、和用户的匹配,每条结论带证据、反面说法和限制,不打分)" })) }),
2600
2619
  output: Type.Object({ runId: Id, workflowId: Id }),
2601
2620
  effects: EGRESS,
2602
2621
  execution: "job",
@@ -2671,6 +2690,31 @@ var tools = {
2671
2690
  execution: "sync",
2672
2691
  errors: ["MISSING_FIELD", "BAD_RUN"]
2673
2692
  }),
2693
+ "employer.compare": defineTool({
2694
+ name: "employer.compare",
2695
+ description: "比较几家上市公司作为雇主的位置(D-29,不打总分,不花模型额度)。每家在自己行业的全部 A 股公司里(A 股按申万一级,新三板按证监会行业,港股美股按 peerIndustry 或一起比的 A 股公司最多的那个申万一级;港美股不披露职工现金,待遇信号缺;yardstick 说明怎么量的,要转述给用户),按四项各算百分位:成长(营收三年复合增速)、研发(研发费用占营收)、待遇信号(每名员工对应的职工现金支出,粗指标)、盈利(净利率);再在三种偏好(成长研发优先 / 均衡 / 稳定回报优先,可另给 weights)下排这几家的名次。under 里每种偏好给:place(按该权重的名次)、usually(权重上下浮动时名次的通常范围)、top3Share(进前三的权重占比,是偏好情景的比例,不是现实概率);缺指标的不估值,给 byKnown / best / worst。outdoneBy 列出在四项上都不差且至少一项更好的公司(不叫梯队)。movedTogether 只说同期一起发生了什么,不是原因,原因要读年报或公告核实。labels 带着规则。未上市公司在 notFound 里,不和上市公司混排。第一次查一个行业约 20–30 秒,之后 7 天内用同一份数据(data 里有取数时间)。",
2696
+ input: Type.Object({
2697
+ companies: Type.Array(Type.String({ description: "公司名、简称或 6 位股票代码" }), { minItems: 1, maxItems: 15 }),
2698
+ weights: Type.Optional(Type.Object({ growth: Type.Optional(Type.Number()), rd: Type.Optional(Type.Number()), pay: Type.Optional(Type.Number()), profit: Type.Optional(Type.Number()) }, { description: "用户自己的偏好权重(相对大小即可,会归一)" })),
2699
+ spread: Type.Optional(Type.Number({ description: "权重上下浮动多少(0.02–1,默认 0.1)" })),
2700
+ year: Type.Optional(Type.Number({ description: "用哪一年的年报,默认最近一个已全部披露的年份" })),
2701
+ peerIndustry: Type.Optional(Type.String({ description: "港股、美股公司拿哪个申万一级行业的 A 股公司当同行(如 机械设备、电子、汽车);不给就用一起比的 A 股公司最多的那个行业" }))
2702
+ }),
2703
+ output: Type.Object({
2704
+ year: Type.Number(),
2705
+ peerGroup: Type.String(),
2706
+ data: Type.Unknown(),
2707
+ measures: Type.Array(Type.Unknown()),
2708
+ companies: Type.Array(Type.Unknown()),
2709
+ scenarios: Type.Unknown(),
2710
+ spread: Type.Number(),
2711
+ notFound: Type.Array(Type.Unknown()),
2712
+ method: Type.String()
2713
+ }),
2714
+ effects: READ,
2715
+ execution: "sync",
2716
+ errors: ["MISSING_FIELD"]
2717
+ }),
2674
2718
  "labor.us": defineTool({
2675
2719
  name: "labor.us",
2676
2720
  description: "美国某些职业的就业人数和年薪(均值、中位数):美国劳工统计局 BLS 的 OEWS 官方年度统计,全国、各行业合计(免费)。按 SOC 职业代码查,一次最多 8 个,如 17-2141 机械工程师、17-2071 电气工程师、17-2072 电子工程师、17-2199 其他工程师、15-1252 软件开发、15-2051 数据科学家、15-1221 计算机与信息研究科学家。返回里的 title 是 BLS 对这个代码的叫法,和你要查的职业不一样就是代码给错了。没有密钥,每天大约只能问 25 次,把要查的职业放进一次调用。",
@@ -3116,9 +3160,9 @@ var tools = {
3116
3160
  }),
3117
3161
  "company.intel": defineTool({
3118
3162
  name: "company.intel",
3119
- description: "企业情况卡(ADR-029):是否上市(A股/港股/新三板/美股)、基本盘(行业/省份/员工数/成立与上市日期)、近三年营收净利与研发费用、分红、近一年公告里的关键词(专精特新/政府补助/IPO/扩产/对外投资/减持等)、专精特新名录匹配;每类各带 fetchedAt 与 ageDays。stale=true 表示没有数据或有一类超过 30 天没更新,此时先 company.intelRefresh。非上市公司只有名录匹配,其余要 research.company 联网调研。列表是摘要:more=true 的类别(新闻、互动易问答全文、研报、股东、分红、公告命中、行业概念)用 company.intelDetail 取全部。",
3163
+ description: "企业情况卡(ADR-029):是否上市(A股/港股/新三板/美股)、基本盘(行业/省份/员工数/成立与上市日期)、近三年营收净利与研发费用、分红、近一年公告里的关键词(专精特新/政府补助/IPO/扩产/对外投资/减持等)、专精特新名录匹配;每类各带 fetchedAt、ageDays 和 everyDays(这一类多久更新一次:新闻公告 1 天、问答研报业绩预告 7 天、在审 IPO 3 天、财报分红股东 30 天、行业员工 90 天、上市信息 180 天)。读的时候立即返回已有的;过期的类(staleKinds)会在后台自动更新(refreshing=true),稍后再读就是新的,一般不用调 company.intelRefresh。非上市公司只有名录匹配,其余要 research.company 联网调研。列表是摘要:more=true 的类别(新闻、互动易问答全文、研报、股东、分红、公告命中、行业概念)用 company.intelDetail 取全部。",
3120
3164
  input: Type.Object({ companyId: Id }),
3121
- output: Type.Object({ companyId: Id, companyName: Type.String(), listing: Type.Unknown(), rows: Type.Array(Type.Object({ kind: Type.String(), data: Type.Unknown(), source: Type.String(), fetchedAt: Type.String(), ageDays: Type.Number(), more: Type.Optional(Type.Boolean()) })), sme: Type.Unknown(), stale: Type.Boolean() }),
3165
+ output: Type.Object({ companyId: Id, companyName: Type.String(), listing: Type.Unknown(), rows: Type.Array(Type.Object({ kind: Type.String(), data: Type.Unknown(), source: Type.String(), fetchedAt: Type.String(), ageDays: Type.Number(), everyDays: Type.Number(), stale: Type.Boolean(), more: Type.Optional(Type.Boolean()) })), sme: Type.Unknown(), stale: Type.Boolean(), staleKinds: Type.Array(Type.String()), refreshing: Type.Boolean() }),
3122
3166
  effects: READ,
3123
3167
  execution: "sync",
3124
3168
  errors: ["NOT_FOUND"]
@@ -3134,8 +3178,8 @@ var tools = {
3134
3178
  }),
3135
3179
  "company.intelRefresh": defineTool({
3136
3180
  name: "company.intelRefresh",
3137
- description: "重新抓取一家公司的企业情况(巨潮、东方财富 F10、新浪财报、巨潮公告;新三板走股转年报 PDF + 东财三板 F10;匿名只读,约 2–30 秒);抓不到的类别保留旧值并在 errors 里说明。不花模型额度。",
3138
- input: Type.Object({ companyId: Id }),
3181
+ description: "立即重新抓取一家公司的企业情况并等它完成(巨潮、东方财富 F10、新浪财报、巨潮公告;新三板走股转年报 PDF + 东财三板 F10;匿名只读,约 2–30 秒)。默认全部类别重抓;force=false 只抓过期的类。抓不到的类别保留旧值并在 errors 里说明。不花模型额度。",
3182
+ input: Type.Object({ companyId: Id, force: Type.Optional(Type.Boolean({ description: "默认 true:全部重抓;false:只抓过期的类" })) }),
3139
3183
  output: Type.Object({ companyId: Id, companyName: Type.String(), listing: Type.Unknown(), rows: Type.Array(Type.Unknown()), sme: Type.Unknown(), stale: Type.Boolean(), errors: Type.Record(Type.String(), Type.String()) }),
3140
3184
  effects: NETWORK_READ,
3141
3185
  execution: "sync",
@@ -3613,6 +3657,15 @@ var tools = {
3613
3657
  execution: "sync",
3614
3658
  errors: ["NOT_AVAILABLE"]
3615
3659
  }),
3660
+ "agent.model": defineTool({
3661
+ name: "agent.model",
3662
+ description: "Zhiro 里用 Claude Code / Codex 时用哪个模型:model 为空 = 跟随它自己的设置;否则用这里选的(只影响 Zhiro 里的用法,不改它自己的设置)。可选的模型见 config.get 的 chat.models.choices。只能由本人。",
3663
+ input: Type.Object({ backend: Type.Union([Type.Literal("claude"), Type.Literal("codex")]), model: Type.String() }),
3664
+ output: Type.Object({ backend: Type.String(), model: Type.String(), using: Type.String() }),
3665
+ effects: WRITE,
3666
+ execution: "sync",
3667
+ errors: []
3668
+ }),
3616
3669
  "agent.harnessConfirm": defineTool({
3617
3670
  name: "agent.harnessConfirm",
3618
3671
  description: "在你自己的 agent(Claude Code / Codex / WorkBuddy 经 MCP)里用 Zhiro 时,确认和撤销在哪里完成:harness = 在 agent 里(agent 复述、你同意后它来确认);window = 只在 Zhiro 窗口。只能由本人。",
@@ -3718,12 +3771,16 @@ export {
3718
3771
  loadSources,
3719
3772
  startSmeResearch,
3720
3773
  smeRun,
3774
+ resolveNeeq,
3721
3775
  INTEL_QUESTION,
3722
3776
  PLACE,
3777
+ normName,
3778
+ smeMatch,
3723
3779
  importSme,
3724
3780
  getIntelDetail,
3725
- getIntel,
3781
+ readIntel,
3726
3782
  refreshIntel,
3783
+ companyFinancials,
3727
3784
  DATA_TOOLS,
3728
3785
  dataFile,
3729
3786
  keepData,
@@ -399,6 +399,31 @@ var MIGRATIONS = [
399
399
  // v36: ADR-050 — every upgrade leaves a line here: when, from / to which schema, by which program, where the original went
400
400
  `
401
401
  create table upgrade_log (at text not null, from_v integer not null, to_v integer not null, app_version text not null, backup text not null default '');
402
+ `,
403
+ // v37: D-27 — BOSS 直聘 visits through the person's agent: one row per one-time permit (issued → used), and the stops
404
+ `
405
+ create table boss_visit (
406
+ id text primary key, session_id text not null, requested_at text not null, not_before text not null, expires_at text not null,
407
+ consumed_at text, target text not null, kind text not null, outcome text not null default 'issued', detail text not null default ''
408
+ );
409
+ create index boss_visit_at on boss_visit(requested_at);
410
+ create index boss_visit_session on boss_visit(session_id);
411
+ `,
412
+ // v38: D-28 — BOSS posts seen on a search page, kept as candidates until their job page is read (then position_id)
413
+ `
414
+ create table boss_candidate (
415
+ job_id text primary key, url text not null, title text not null, salary text not null default '', city text not null default '',
416
+ district text not null default '', experience text not null default '', degree text not null default '', days text not null default '',
417
+ months text not null default '', brand text not null default '', scale text not null default '', stage text not null default '',
418
+ industry text not null default '', skills text not null default '[]', labels text not null default '[]', query text not null default '',
419
+ city_code text not null default '', first_seen text not null, last_seen text not null, position_id text, detail_at text
420
+ );
421
+ create index boss_candidate_seen on boss_candidate(last_seen);
422
+ `,
423
+ // v39: D-29 — the A-share list and one industry's statements for a year, kept with the time they were fetched, so a
424
+ // comparison of employers can say which data it used and be run again on the same data
425
+ `
426
+ create table peer_snapshot (key text primary key, fetched_at text not null, data_json text not null);
402
427
  `
403
428
  ];
404
429
  var COUNTS_MAY_CHANGE = {};
@@ -2,7 +2,7 @@ import {
2
2
  json,
3
3
  now,
4
4
  tx
5
- } from "./chunk-5KCN32HQ.js";
5
+ } from "./chunk-4EPNDXRS.js";
6
6
  import {
7
7
  updateConfig
8
8
  } from "./chunk-4JJEOOW2.js";
@@ -832,12 +832,12 @@ var CatalogBatch = class {
832
832
  };
833
833
 
834
834
  // apps/daemon/src/data/sheet.ts
835
- function parseCsv(text2) {
835
+ function parseCsv(text3) {
836
836
  const rows = [];
837
837
  let row = [];
838
838
  let field = "";
839
839
  let inQ = false;
840
- const s = text2.replace(/^/, "");
840
+ const s = text3.replace(/^/, "");
841
841
  for (let i = 0; i < s.length; i++) {
842
842
  const c = s[i];
843
843
  if (inQ) {
@@ -866,8 +866,8 @@ function parseCsv(text2) {
866
866
  return rows.filter((r) => r.some((f) => f.trim() !== ""));
867
867
  }
868
868
  __name(parseCsv, "parseCsv");
869
- function parseDeadline(text2) {
870
- const t = (text2 ?? "").trim();
869
+ function parseDeadline(text3) {
870
+ const t = (text3 ?? "").trim();
871
871
  if (!t) return { closesAt: null, kind: "unknown" };
872
872
  if (/招满|长期|滚动|持续|尽快|随时/.test(t)) return { closesAt: null, kind: /滚动|持续|尽快|随时/.test(t) ? "rolling" : "until_filled" };
873
873
  const m = t.match(/(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})/);
@@ -1005,8 +1005,8 @@ function listingsFromTable(table, cfg, publisher) {
1005
1005
  return tableToListings(table, columns, cfg, void 0, publisher);
1006
1006
  }
1007
1007
  __name(listingsFromTable, "listingsFromTable");
1008
- function csvToTable(text2) {
1009
- const [head = [], ...rows] = parseCsv(text2);
1008
+ function csvToTable(text3) {
1009
+ const [head = [], ...rows] = parseCsv(text3);
1010
1010
  const headers = head.map((h) => h.trim());
1011
1011
  const at = new Map(headers.map((h, i) => [h, i]));
1012
1012
  return { headers, rows: rows.map((r) => ({ id: "", get: /* @__PURE__ */ __name((h) => (r[at.get(h) ?? -1] ?? "").trim(), "get") })) };
@@ -1244,7 +1244,7 @@ function zhilianUrl(url) {
1244
1244
  }
1245
1245
  __name(zhilianUrl, "zhilianUrl");
1246
1246
  var ID = /(?:xiaozhaoId|companyXzId|companyOutId|rootCompanyId|orgNumbers?)["']?[:=]\[?["']?(\d{5,7})\b|globalData\.companyId,[\w$]+="?(\d{5,7})\b/g;
1247
- var idsIn = /* @__PURE__ */ __name((text2) => [...new Set([...text2.matchAll(ID)].map((m) => m[1] || m[2]))], "idsIn");
1247
+ var idsIn = /* @__PURE__ */ __name((text3) => [...new Set([...text3.matchAll(ID)].map((m) => m[1] || m[2]))], "idsIn");
1248
1248
  async function count(ctx, id) {
1249
1249
  const r = await ctx.fetchJson(API2, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ orgNumbers: [id], jobSource: 2, pageIndex: 1, pageSize: 1 }) }).catch(() => null);
1250
1250
  return r?.data?.pageInfo?.totalNum ?? 0;
@@ -1818,9 +1818,9 @@ function literalToJson(s) {
1818
1818
  return o;
1819
1819
  }
1820
1820
  __name(literalToJson, "literalToJson");
1821
- function siteLinks(text2, base) {
1821
+ function siteLinks(text3, base) {
1822
1822
  const root = new URL(".", base).href, out = [];
1823
- for (const m of text2.matchAll(/["']((?:\.\/)?[\w./-]+\.(?:html?|js))(?:\?[^"']*)?["']/g)) {
1823
+ for (const m of text3.matchAll(/["']((?:\.\/)?[\w./-]+\.(?:html?|js))(?:\?[^"']*)?["']/g)) {
1824
1824
  const p = new URL(m[1], base).href;
1825
1825
  if (p.startsWith(root) && !out.includes(p) && !/jquery|swiper|jweixin|\.min\.js/.test(p)) out.push(p);
1826
1826
  }
@@ -2076,9 +2076,140 @@ var csv = {
2076
2076
  }
2077
2077
  };
2078
2078
 
2079
+ // apps/daemon/src/data/readers/gaoxiaojob.ts
2080
+ var M = "https://m.gaoxiaojob.com";
2081
+ var W = "https://www.gaoxiaojob.com";
2082
+ var UA3 = { "user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148" };
2083
+ var DAY = 864e5;
2084
+ var LONG = /长期有效|长期招聘|常年|招满为止|招满即止|额满为止/;
2085
+ var columnOf = /* @__PURE__ */ __name((url) => /^https?:\/\/(?:www\.|m\.)?gaoxiaojob\.com\/column\/(\d+)\.html/.exec(url.trim())?.[1] ?? null, "columnOf");
2086
+ function dayOf(mmdd, today = /* @__PURE__ */ new Date()) {
2087
+ const m = /^(\d{1,2})-(\d{1,2})$/.exec(mmdd.trim());
2088
+ if (!m) return null;
2089
+ const y = today.getUTCFullYear(), mm = m[1].padStart(2, "0"), dd = m[2].padStart(2, "0");
2090
+ const d = `${y}-${mm}-${dd}`;
2091
+ return Date.parse(d) > today.getTime() + DAY ? `${y - 1}-${mm}-${dd}` : d;
2092
+ }
2093
+ __name(dayOf, "dayOf");
2094
+ var text2 = /* @__PURE__ */ __name((html) => html.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>/g, " ").replace(/<br\s*\/?>|<\/p>|<\/div>|<\/li>/g, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/[ \t\r]+/g, " ").replace(/\n\s*/g, "\n"), "text");
2095
+ function parseNotice(id, title, html) {
2096
+ const t = text2(html);
2097
+ const flat = t.replace(/\s+/g, " ");
2098
+ const field = /* @__PURE__ */ __name((k) => new RegExp(`${k}[::]\\s*(.*?)\\s+(?:截止日期|学历要求|所属省份|工作地点|报名方式|栏目分类|需求学科|发布时间)`).exec(flat)?.[1]?.trim() ?? "", "field");
2099
+ const posted = /发布时间[::]\s*(\d{4}-\d{2}-\d{2})/.exec(flat)?.[1] ?? null;
2100
+ const deadlineText = field("截止日期");
2101
+ const full = /(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})/.exec(deadlineText);
2102
+ const closesAt = full ? `${full[1]}-${full[2].padStart(2, "0")}-${full[3].padStart(2, "0")}` : null;
2103
+ const source = /来源链接[::]\s*(https?:\/\/\S+)/.exec(flat)?.[1] ?? "";
2104
+ const a = t.indexOf("公告详情"), b = t.indexOf("来源于网络", a);
2105
+ const body = (a >= 0 ? t.slice(a + 4, b > a ? b : a + 8e3) : "").trim().slice(0, 8e3);
2106
+ return { id, title, posted, deadlineText, closesAt, place: field("工作地点"), source, body };
2107
+ }
2108
+ __name(parseNotice, "parseNotice");
2109
+ function stillOpen(n, listedOn, today = /* @__PURE__ */ new Date()) {
2110
+ if (n.closesAt) return Date.parse(`${n.closesAt}T23:59:59+08:00`) >= today.getTime();
2111
+ const on = n.posted ?? listedOn;
2112
+ if (!on) return false;
2113
+ const age = (today.getTime() - Date.parse(on)) / DAY;
2114
+ return age <= (LONG.test(n.deadlineText) ? 180 : 90);
2115
+ }
2116
+ __name(stillOpen, "stillOpen");
2117
+ function noticeListings(n, jobs, cfg) {
2118
+ const company = (jobs.find((j) => j.companyName)?.companyName ?? "").replace(/#+$/, "").trim() || unitFrom(n.title);
2119
+ const base = {
2120
+ company,
2121
+ noticeUrl: `${W}/announcement/detail/${n.id}.html`,
2122
+ datePosted: n.posted ?? void 0,
2123
+ validThrough: n.closesAt ?? void 0,
2124
+ deadlineText: n.deadlineText || void 0,
2125
+ cohort: cfg.cohort,
2126
+ batch: n.title.slice(0, 80),
2127
+ // one announcement, one campaign (campaigns group by publisher|company|cohort|batch)
2128
+ publisher: "gaoxiaojob",
2129
+ grade: "聚合",
2130
+ isOriginal: false,
2131
+ confidence: 0.7
2132
+ // a reprint: below the 0.85–0.9 the official readers give
2133
+ };
2134
+ const intern = /* @__PURE__ */ __name((s) => /实习/.test(s) ? "实习" : void 0, "intern");
2135
+ const jd = [n.body, n.source ? `原公告:${n.source}` : ""].filter(Boolean).join("\n\n");
2136
+ if (!jobs.length) return [{ ...base, externalId: `ann:${n.id}`, url: base.noticeUrl, title: n.title, locations: n.place ? [n.place] : [], description: jd, kind: intern(n.title) }];
2137
+ return jobs.map((j) => ({
2138
+ ...base,
2139
+ externalId: `job:${j.jobId}`,
2140
+ url: j.url ? `${W}${j.url}` : base.noticeUrl,
2141
+ title: j.jobName,
2142
+ locations: j.city ? [j.city] : n.place ? [n.place] : [],
2143
+ description: jd,
2144
+ degreeReq: j.education || void 0,
2145
+ majorReq: j.major || void 0,
2146
+ kind: intern(`${j.jobName} ${n.title}`)
2147
+ }));
2148
+ }
2149
+ __name(noticeListings, "noticeListings");
2150
+ var unitFrom = /* @__PURE__ */ __name((title) => /^(.+?)(?:\d{4}|诚聘|招聘|公开|引进|招收)/.exec(title)?.[1]?.trim() || title.slice(0, 30), "unitFrom");
2151
+ async function jobsOf(ctx, id) {
2152
+ const out = [];
2153
+ for (let page2 = 1; page2 <= 20; page2++) {
2154
+ const d = await ctx.fetchJson(`${M}/announcement/get-job-list?announcementId=${id}&page=${page2}`, { headers: UA3 });
2155
+ const list = d?.data?.list ?? [];
2156
+ out.push(...list);
2157
+ if (!list.length || out.length >= Number(d?.data?.count ?? 0)) return out;
2158
+ await ctx.sleep(pause());
2159
+ }
2160
+ return out;
2161
+ }
2162
+ __name(jobsOf, "jobsOf");
2163
+ var pause = /* @__PURE__ */ __name(() => 600 + Math.floor(Math.random() * 800), "pause");
2164
+ var gaoxiaojob = {
2165
+ id: "gaoxiaojob",
2166
+ name: "高校人才网",
2167
+ input: "www.gaoxiaojob.com/column/<栏目编号>.html(如武汉 48、武汉科研院所 509、武汉企业 511)",
2168
+ mode: "pull",
2169
+ describe: "高校人才网的一个栏目(研究院、实验室、高校团队、事业单位、企业):读手机版公开列表和每条公告的岗位清单,只收仍有效的;转载来源,记为聚合,零 token",
2170
+ detect: /* @__PURE__ */ __name((url) => !!columnOf(url), "detect"),
2171
+ siteKey: /* @__PURE__ */ __name((url) => `column/${columnOf(url)}`, "siteKey"),
2172
+ async fetch(cfg, ctx) {
2173
+ const col = columnOf(cfg.url ?? "");
2174
+ if (!col) throw new Error("SCHEMA_CHANGED: 不是高校人才网的栏目链接");
2175
+ const maxPages = cfg.maxPages ?? 60;
2176
+ const today = /* @__PURE__ */ new Date();
2177
+ const out = [];
2178
+ let oldPages = 0, page2 = 1;
2179
+ for (; page2 <= maxPages; page2++) {
2180
+ const d = await ctx.fetchJson(`${M}/home/get-announcement-list?columnId=${col}&isEstablishment=0&announcementHeat=0&page=${page2}`, { headers: UA3 });
2181
+ if (d?.code !== 200) throw new Error(`SCHEMA_CHANGED: 列表接口返回 ${d?.code ?? "?"} ${d?.msg ?? ""}`);
2182
+ const items = d?.data ?? [];
2183
+ if (!items.length) break;
2184
+ let fresh = 0;
2185
+ for (const it of items) {
2186
+ const listedOn = dayOf(it.refreshTime ?? "", today);
2187
+ if (listedOn && (today.getTime() - Date.parse(listedOn)) / DAY > 180) continue;
2188
+ fresh++;
2189
+ await ctx.sleep(pause());
2190
+ const n = parseNotice(
2191
+ String(it.id),
2192
+ it.title,
2193
+ await ctx.fetchText(`${W}/announcement/detail/${it.id}.html`)
2194
+ /* the desktop page: its 基本信息 block is what parseNotice reads */
2195
+ );
2196
+ if (!stillOpen(n, listedOn, today)) continue;
2197
+ await ctx.sleep(pause());
2198
+ out.push(...noticeListings(n, await jobsOf(ctx, n.id), cfg));
2199
+ }
2200
+ ctx.log(`gaoxiaojob ${col}: page ${page2} ${items.length} 条,在窗口内 ${fresh}(累计 ${out.length})`);
2201
+ oldPages = fresh ? 0 : oldPages + 1;
2202
+ if (oldPages >= 2) break;
2203
+ await ctx.sleep(pause());
2204
+ }
2205
+ if (page2 > maxPages) throw new Error(`TIMEOUT: 读到第 ${maxPages} 页还没读完这个栏目,这次不算(读一半会把另一半标成下架)`);
2206
+ return out;
2207
+ }
2208
+ };
2209
+
2079
2210
  // apps/daemon/src/data/readers/index.ts
2080
2211
  var capturePush = { id: "capture", name: "任意页面抓取", mode: "push", describe: "任意招聘页/简章:书签一键捕获,或给 URL 由 daemon 抓取(公众号/就业网/官网),模型抽取岗位" };
2081
- var sources = Object.fromEntries([moka, beisen, feishuBase, feishuJobs, zhilian, iguopin, chnenergy, chng, dayee, job51, kuaishou, xiaohongshu, csv, capturePush].map((s) => [s.id, s]));
2212
+ var sources = Object.fromEntries([moka, beisen, feishuBase, feishuJobs, zhilian, iguopin, chnenergy, chng, dayee, job51, kuaishou, xiaohongshu, gaoxiaojob, csv, capturePush].map((s) => [s.id, s]));
2082
2213
  function sourceFor(url) {
2083
2214
  return Object.values(sources).find((s) => s.detect?.(url));
2084
2215
  }
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  DomainError,
3
3
  NotFound
4
- } from "./chunk-LH25QWMP.js";
4
+ } from "./chunk-ZKD2BKN3.js";
5
5
  import {
6
6
  now
7
- } from "./chunk-5KCN32HQ.js";
7
+ } from "./chunk-4EPNDXRS.js";
8
8
  import {
9
9
  __name
10
10
  } from "./chunk-2SSYR7VW.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-NNH4ZG6F.js";
3
+ } from "./chunk-H3EFVYGD.js";
4
4
  import {
5
5
  COUNTS_MAY_CHANGE,
6
6
  MIGRATIONS,
@@ -9,7 +9,7 @@ import {
9
9
  migrate,
10
10
  openDb,
11
11
  schemaOf
12
- } from "./chunk-5KCN32HQ.js";
12
+ } from "./chunk-4EPNDXRS.js";
13
13
  import {
14
14
  configFile,
15
15
  dbPath,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  json,
3
3
  now
4
- } from "./chunk-5KCN32HQ.js";
4
+ } from "./chunk-4EPNDXRS.js";
5
5
  import {
6
6
  __name
7
7
  } from "./chunk-2SSYR7VW.js";
@@ -1,5 +1,5 @@
1
1
  // apps/daemon/src/version.ts
2
- var VERSION = true ? "0.1.2" : JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version;
2
+ var VERSION = true ? "0.2.1" : JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).version;
3
3
 
4
4
  export {
5
5
  VERSION