{
  "always_run_in_app": false,
  "icon": {
    "color": "deep-blue",
    "glyph": "chart-line"
  },
  "name": "行情",
  "script": "// market-widget.js — Scriptable 桌面小组件：美股 + Crypto 行情\n//\n// 安装：手机 Safari 打开面板(https://widget.sudoo.dev) → 一键安装，别手抄这段代码。\n//   桌面长按空白 → 添加小组件 → Scriptable 中号 → 长按「编辑小组件」→ Script 选「行情」\n// 叠放轮换：拖到另一个同尺寸小组件上即组成叠放，上下滑切换；编辑叠放可开「智能轮换」\n// 省电设计：每次刷新 1 个请求(Worker 模式)；MIN_FETCH_MIN 内重复触发直接读缓存；\n//   美股盘中/盘后自适应刷新间隔；请求失败回退本地缓存；深色模式纯黑背景(OLED 省电)\n\n// ===================== 配置 =====================\n// 显示哪些、以什么顺序显示，都在网页面板上调(https://widget.sudoo.dev)，存在 Worker 的 KV 里。\n// 改配置不用动这个脚本；下面的 ITEMS 只在直连模式(WORKER_URL 置空)下才生效。\nconst WORKER_URL = \"https://widget.sudoo.dev/api/market\"; // 置空则直连数据源\nconst PANEL_URL = /\\/api\\/market(?:\\?.*)?$/.test(WORKER_URL)\n  ? WORKER_URL.replace(/\\/api\\/market(?:\\?.*)?$/, \"/\")\n  : \"\";\nconst RED_UP = false;            // false=绿涨红跌(美股/币圈惯例) true=红涨绿跌(A股惯例)\nconst REFRESH_OPEN_MIN = 15;     // 美股盘中建议刷新间隔(分钟)。iOS 有刷新配额，<15 意义不大\nconst REFRESH_CLOSED_MIN = 60;   // 盘后/周末刷新间隔(分钟)，越大越省电\nconst MIN_FETCH_MIN = 5;         // 数据新鲜期：n 分钟内重复运行不发网络请求\n\n// --- 以下仅直连模式(WORKER_URL=\"\")使用 ---\n// 股票和币可以任意交错，顺序即显示顺序。coin 的 id 用 CoinGecko 的(bitcoin 而非 BTC)。\nconst ITEMS = [\n  { type: \"stock\", id: \"QQQ\" },\n  { type: \"stock\", id: \"AAPL\" },\n  { type: \"stock\", id: \"TSLA\" },\n  { type: \"stock\", id: \"NVDA\" },\n  { type: \"coin\", id: \"bitcoin\" },\n  { type: \"coin\", id: \"ethereum\" },\n  { type: \"coin\", id: \"solana\" },\n];\nconst STOCK_SOURCE = \"tencent\";  // \"tencent\"=腾讯行情(国内直连，默认) | \"yahoo\"=雅虎(需海外网络)\n// ================================================\n\nconst ID_SYM = {\n  bitcoin: \"BTC\", ethereum: \"ETH\", solana: \"SOL\", binancecoin: \"BNB\", ripple: \"XRP\",\n  dogecoin: \"DOGE\", cardano: \"ADA\", \"avalanche-2\": \"AVAX\", chainlink: \"LINK\", sui: \"SUI\",\n  \"the-open-network\": \"TON\", tron: \"TRX\", litecoin: \"LTC\", hyperliquid: \"HYPE\",\n  polkadot: \"DOT\", \"matic-network\": \"MATIC\", uniswap: \"UNI\", aave: \"AAVE\",\n  \"shiba-inu\": \"SHIB\", aptos: \"APT\",\n};\nconst coinSym = (id) => ID_SYM[id] || id.slice(0, 4).toUpperCase();\n\nconst fm = FileManager.local();\nconst CACHE_PATH = fm.joinPath(fm.libraryDirectory(), \"market-widget-cache.json\");\n\nconst PRIMARY = Color.dynamic(new Color(\"#1c1c1e\"), new Color(\"#ffffff\"));\nconst SECONDARY = Color.dynamic(new Color(\"#8e8e93\"), new Color(\"#98989e\"));\nconst WARN = new Color(\"#ff9f0a\");\n\n// ---------- 网络 ----------\nfunction makeReq(url) {\n  const r = new Request(url);\n  r.timeoutInterval = 12;\n  r.headers = {\n    \"User-Agent\":\n      \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15\",\n  };\n  return r;\n}\n\n// Worker 聚合模式：不带参数=用面板存在 KV 里的配置(含顺序)；单请求拿回有序的 items\nasync function fetchViaWorker() {\n  const j = await makeReq(WORKER_URL).loadJSON();\n  if (!j.items || !j.items.length) throw new Error(\"Worker 未返回数据\");\n  // Older Workers didn't return `requested`. Falling back to successful items keeps\n  // compatibility, but only newer responses can safely preserve a temporarily missing row.\n  const requested = Array.isArray(j.requested) ? j.requested : j.items;\n  return { items: j.items, requested };\n}\n\n// --- 直连模式 ---\n// 常用指数简称 → 雅虎代码；腾讯源 usVIX/usDJI 直接可用，无需映射\nconst INDEX_YAHOO = { VIX: \"^VIX\", SPX: \"^GSPC\", GSPC: \"^GSPC\", DJI: \"^DJI\", IXIC: \"^IXIC\", NDX: \"^NDX\", RUT: \"^RUT\" };\nconst yahooSymbol = (id) => (id.startsWith(\"^\") ? id : INDEX_YAHOO[id] || id);\nconst stripCaret = (id) => id.replace(/^\\^/, \"\");\n\nasync function fetchStocksYahoo(symbols) {\n  const url =\n    \"https://query1.finance.yahoo.com/v8/finance/spark?symbols=\" +\n    encodeURIComponent(symbols.map(yahooSymbol).join(\",\")) +\n    \"&range=1d&interval=30m\";\n  const j = await makeReq(url).loadJSON();\n  const out = [];\n  for (const sym of symbols) {\n    const d = j[yahooSymbol(sym)];\n    const closes = ((d && d.close) || []).filter((x) => x != null);\n    if (!closes.length) continue;\n    const price = closes[closes.length - 1];\n    const prev = d.chartPreviousClose ?? d.previousClose;\n    out.push({ type: \"stock\", id: sym, sym: stripCaret(sym), price, chg: prev ? (price / prev - 1) * 100 : 0 });\n  }\n  if (!out.length) throw new Error(\"Yahoo 无数据\");\n  return out;\n}\n\n// 腾讯行情：国内直连。字段以 ~ 分隔，3=现价 4=昨收 32=涨跌幅%。\n// 指数的代码字段带前导点(\".VIX\")，直接 split(\".\") 会得到空串把它丢掉。\nasync function fetchStocksTencent(symbols) {\n  const url = \"https://qt.gtimg.cn/q=\" + symbols.map((s) => \"us\" + stripCaret(s)).join(\",\");\n  const text = await makeReq(url).loadString();\n  const map = {};\n  const re = /v_us([\\w.]+)=\"([^\"]*)\"/g;\n  let m;\n  while ((m = re.exec(text)) !== null) {\n    const f = m[2].split(\"~\");\n    const price = parseFloat(f[3]);\n    const prev = parseFloat(f[4]);\n    let chg = parseFloat(f[32]);\n    if (!isFinite(chg)) chg = isFinite(prev) && prev ? (price / prev - 1) * 100 : 0;\n    if (!isFinite(price)) continue;\n    const code = (f[2] || m[1]).replace(/^\\./, \"\");\n    const sym = code.split(\".\")[0].toUpperCase();\n    if (sym) map[sym] = { price, chg };\n  }\n  const out = [];\n  for (const s of symbols) {\n    const hit = map[stripCaret(s).toUpperCase()];\n    if (hit) out.push({ type: \"stock\", id: s, sym: stripCaret(s), price: hit.price, chg: hit.chg });\n  }\n  if (!out.length) throw new Error(\"腾讯行情无数据\");\n  return out;\n}\n\n// 路由：普通代码走腾讯(国内直连)；指数+腾讯缺的代码走雅虎\n// (腾讯的指数报价涨跌恒为 0 且价格可能滞后，所以指数优先雅虎)；\n// 雅虎也拿不到时再用腾讯的指数报价兜底，聊胜于无\nasync function fetchStocksDirect(symbols) {\n  if (STOCK_SOURCE === \"yahoo\") return fetchStocksYahoo(symbols);\n  const isIdx = (s) => s.startsWith(\"^\") || INDEX_YAHOO[s];\n  const got = {};\n  const grab = async (fetcher, want) => {\n    if (!want.length) return;\n    try {\n      for (const q of await fetcher(want)) got[q.id] = q;\n    } catch (e) {}\n  };\n  await grab(fetchStocksTencent, symbols.filter((s) => !isIdx(s)));\n  await grab(fetchStocksYahoo, symbols.filter((s) => !got[s]));\n  await grab(fetchStocksTencent, symbols.filter((s) => isIdx(s) && !got[s]));\n  const out = symbols.map((s) => got[s]).filter(Boolean);\n  if (!out.length) throw new Error(\"美股数据源全部失败\");\n  return out;\n}\n\nasync function fetchCoinsGecko(ids) {\n  const url =\n    \"https://api.coingecko.com/api/v3/simple/price?ids=\" +\n    encodeURIComponent(ids.join(\",\")) +\n    \"&vs_currencies=usd&include_24hr_change=true\";\n  const j = await makeReq(url).loadJSON();\n  return ids.map((id) => {\n    const d = j[id];\n    if (!d || d.usd == null) throw new Error(\"CoinGecko 缺少 \" + id);\n    return { type: \"coin\", id, sym: coinSym(id), price: d.usd, chg: d.usd_24h_change ?? 0 };\n  });\n}\n\n// Coinbase 兜底：每币一个请求。不用币安——它 403 拒绝很多云出口 IP。\nasync function fetchCoinsCoinbase(ids) {\n  const list = await Promise.all(\n    ids.map(async (id) => {\n      const sym = coinSym(id);\n      try {\n        const j = await makeReq(\n          \"https://api.exchange.coinbase.com/products/\" + sym + \"-USD/stats\"\n        ).loadJSON();\n        const last = parseFloat(j.last);\n        const open = parseFloat(j.open);\n        if (!isFinite(last)) return null;\n        return {\n          type: \"coin\", id, sym, price: last,\n          chg: isFinite(open) && open ? (last / open - 1) * 100 : 0,\n        };\n      } catch (e) {\n        return null;\n      }\n    })\n  );\n  const out = list.filter(Boolean);\n  if (!out.length) throw new Error(\"Coinbase 无数据\");\n  return out;\n}\n\n// 直连模式：分两批取数，再按 ITEMS 的顺序拼回去\nasync function fetchDirect() {\n  const stockIds = ITEMS.filter((i) => i.type === \"stock\").map((i) => i.id);\n  const coinIds = ITEMS.filter((i) => i.type === \"coin\").map((i) => i.id);\n  const [s, c] = await Promise.all([\n    stockIds.length ? fetchStocksDirect(stockIds).catch(() => null) : null,\n    coinIds.length\n      ? fetchCoinsGecko(coinIds).catch(() => fetchCoinsCoinbase(coinIds)).catch(() => null)\n      : null,\n  ]);\n  const byKey = {};\n  for (const q of [...(s || []), ...(c || [])]) byKey[q.type + \":\" + q.id] = q;\n  const quotes = ITEMS.map((i) => byKey[i.type + \":\" + i.id]).filter(Boolean);\n  if (!quotes.length) throw new Error(\"数据源全部失败\");\n  return { items: quotes, requested: ITEMS };\n}\n\n// ---------- 缓存 ----------\nfunction quoteKey(it) {\n  return it.type + \":\" + it.id;\n}\n\nfunction loadCache() {\n  try {\n    if (!fm.fileExists(CACHE_PATH)) return null;\n    const j = JSON.parse(fm.readString(CACHE_PATH));\n    if (!j || !j.ts || !Array.isArray(j.items) || !j.items.length) return null;\n    return {\n      ts: j.ts,\n      items: j.items,\n      requested: Array.isArray(j.requested) ? j.requested : null,\n    };\n  } catch (e) {\n    return null;\n  }\n}\n\nfunction saveCache(d) {\n  try {\n    fm.writeString(CACHE_PATH, JSON.stringify(d));\n  } catch (e) {}\n}\n\nfunction staleCount(data) {\n  return data.items.filter((it) => it.stale).length;\n}\n\nfunction ageLabel(ms) {\n  const mins = Math.max(0, Math.round(ms / 60e3));\n  if (mins < 1) return \"刚刚\";\n  if (mins < 60) return mins + \"分\";\n  const hours = Math.floor(mins / 60);\n  if (hours < 24) return hours + \"时\";\n  return Math.floor(hours / 24) + \"天\";\n}\n\nfunction dataNote(data) {\n  const stale = staleCount(data);\n  const requested = Array.isArray(data.requested) ? data.requested.length : data.items.length;\n  const missing = Math.max(0, requested - data.items.length);\n  if (!stale && !missing) return \"\";\n  if (stale && missing) return stale + \"缓存/\" + missing + \"缺\";\n  if (missing) return missing + \"项无数据\";\n  if (stale < data.items.length) return stale + \"项缓存\";\n  const oldest = Math.min(...data.items.map((it) => it.updatedAt || data.ts));\n  return \"缓存\" + ageLabel(Date.now() - oldest);\n}\n\n/** Keep every requested row when possible; mark rows that came from the previous cache. */\nfunction mergeLiveWithCache(live, requested, cache, now) {\n  const fresh = new Map(live.map((it) => [quoteKey(it), it]));\n  const old = new Map((cache ? cache.items : []).map((it) => [quoteKey(it), it]));\n  const items = [];\n  for (const req of requested) {\n    const k = quoteKey(req);\n    const hit = fresh.get(k);\n    if (hit) {\n      items.push({ ...hit, stale: false, updatedAt: now });\n      continue;\n    }\n    const prev = old.get(k);\n    if (prev) items.push({ ...prev, stale: true, updatedAt: prev.updatedAt || cache.ts });\n  }\n  return items;\n}\n\nfunction markCacheStale(cache) {\n  return {\n    ...cache,\n    // Throttle the next run while keeping each quote's real `updatedAt` for the age label.\n    ts: Date.now(),\n    items: cache.items.map((it) => ({\n      ...it,\n      stale: true,\n      updatedAt: it.updatedAt || cache.ts,\n    })),\n  };\n}\n\nasync function getData() {\n  const cache = loadCache();\n  const now = Date.now();\n  // 新鲜缓存直接用：叠放翻页/反复进 app 不会重复打接口\n  if (cache && now - cache.ts < MIN_FETCH_MIN * 60e3) {\n    return { ...cache, note: dataNote(cache) };\n  }\n\n  const live = await (WORKER_URL ? fetchViaWorker() : fetchDirect()).catch(() => null);\n  if (live && live.items.length) {\n    const requested = live.requested.length ? live.requested : live.items;\n    const d = {\n      ts: now,\n      requested,\n      items: mergeLiveWithCache(live.items, requested, cache, now),\n    };\n    saveCache(d);\n    return { ...d, note: dataNote(d) };\n  }\n  // 取数失败不该让桌面变白：有缓存就接着用，并标注出来\n  if (cache) {\n    const d = markCacheStale(cache);\n    return { ...d, note: dataNote(d) };\n  }\n  throw new Error(\"网络请求失败，且无本地缓存\");\n}\n\n// ---------- 美股交易时段(决定标题和刷新频率) ----------\nfunction usMarketStatus() {\n  try {\n    const parts = {};\n    const fmt = new Intl.DateTimeFormat(\"en-US\", {\n      timeZone: \"America/New_York\",\n      weekday: \"short\",\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      hour12: false,\n    });\n    for (const p of fmt.formatToParts(new Date())) parts[p.type] = p.value;\n    if (parts.weekday === \"Sat\" || parts.weekday === \"Sun\") {\n      return { label: \"休市\", open: false };\n    }\n    const mins = ((parseInt(parts.hour, 10) % 24) * 60) + parseInt(parts.minute, 10);\n    if (mins >= 570 && mins < 960) return { label: \"盘中\", open: true }; // ET 9:30–16:00\n    if (mins >= 240 && mins < 570) return { label: \"盘前\", open: false }; // ET 4:00–9:30\n    if (mins >= 960 && mins < 1200) return { label: \"盘后\", open: false }; // ET 16:00–20:00\n    return { label: \"休市\", open: false };\n  } catch (e) {\n    // 无 Intl 时的粗略回退：UTC 13:30–21:00 当作盘中\n    const d = new Date();\n    if (d.getUTCDay() === 0 || d.getUTCDay() === 6) return { label: \"休市\", open: false };\n    const m = d.getUTCHours() * 60 + d.getUTCMinutes();\n    const open = m >= 810 && m < 1260;\n    return { label: open ? \"盘中\" : \"休市\", open };\n  }\n}\n\n// ---------- 格式化 ----------\nconst pad2 = (n) => String(n).padStart(2, \"0\");\n\nfunction fmtPrice(p) {\n  // 五位数以上小数没有信息量，还挤窄小号的宽度(63,613.4 → 63,613)\n  if (p >= 10000) return Math.round(p).toLocaleString(\"en-US\");\n  if (p >= 1000) return p.toLocaleString(\"en-US\", { maximumFractionDigits: 1 });\n  if (p >= 1) return p.toFixed(2);\n  return p.toPrecision(3);\n}\n\nfunction fmtChg(c) {\n  return (c >= 0 ? \"+\" : \"\") + c.toFixed(2) + \"%\";\n}\n\nconst GREEN = {\n  fg: Color.dynamic(new Color(\"#1e9e4e\"), new Color(\"#30d158\")),\n  bg: Color.dynamic(new Color(\"#1e9e4e\", 0.12), new Color(\"#30d158\", 0.18)),\n};\nconst RED = {\n  fg: Color.dynamic(new Color(\"#e0362f\"), new Color(\"#ff453a\")),\n  bg: Color.dynamic(new Color(\"#e0362f\", 0.12), new Color(\"#ff453a\", 0.18)),\n};\n\nfunction chgColors(chg) {\n  const up = RED_UP ? RED : GREEN;\n  const down = RED_UP ? GREEN : RED;\n  return chg >= 0 ? up : down;\n}\n\n// ---------- 渲染 ----------\n// 每个尺寸能放下几行、几列。顺序是配置给的，这里只负责往格子里灌。\n// 小号最多 6 行，字号随条数三档走(4/5/6 行→12/11/10pt)：\n// 行距靠弹性 spacer 均摊，字号不跟着放大会显得字小间距大、比例失衡。\nfunction layoutFor(family, count) {\n  if (family === \"small\") {\n    if (count <= 4) return { cols: 1, rows: 4, fSym: 12, fPrice: 12, fChg: 9, pill: false, gap: 4 };\n    if (count === 5) return { cols: 1, rows: 5, fSym: 11, fPrice: 11, fChg: 9, pill: false, gap: 3 };\n    return { cols: 1, rows: 6, fSym: 10, fPrice: 10, fChg: 8, pill: false, gap: 3 };\n  }\n  if (family === \"large\") return { cols: 2, rows: 8, fSym: 15, fPrice: 15, fChg: 12, pill: true, gap: 7 };\n  return { cols: 2, rows: 4, fSym: 12, fPrice: 12, fChg: 10, pill: true, gap: 5 };\n}\n\n// 先填满左列再填右列，两列尽量均分(6 项 = 左 3 右 3)。超出容量的项直接不显示。\nfunction splitColumns(items, { cols, rows }) {\n  const shown = items.slice(0, cols * rows);\n  if (cols === 1) return [shown];\n  const left = Math.ceil(shown.length / 2);\n  return [shown.slice(0, left), shown.slice(left)];\n}\n\nfunction addRow(col, it, L) {\n  const row = col.addStack();\n  row.centerAlignContent();\n  const sym = row.addText(it.sym);\n  sym.font = Font.semiboldSystemFont(L.fSym);\n  sym.textColor = PRIMARY;\n  sym.lineLimit = 1;\n  row.addSpacer();\n  const price = row.addText((it.stale ? \"~\" : \"\") + fmtPrice(it.price));\n  price.font = Font.mediumMonospacedSystemFont(L.fPrice);\n  price.textColor = it.stale ? SECONDARY : PRIMARY;\n  price.lineLimit = 1;\n  price.minimumScaleFactor = 0.7;\n  row.addSpacer(L.pill ? 6 : 5);\n  const c = chgColors(it.chg);\n  if (L.pill) {\n    const chip = row.addStack();\n    chip.setPadding(2, 5, 2, 5);\n    chip.cornerRadius = 5;\n    chip.backgroundColor = c.bg;\n    const ct = chip.addText(fmtChg(it.chg));\n    ct.font = Font.boldMonospacedSystemFont(L.fChg);\n    ct.textColor = c.fg;\n  } else {\n    const ct = row.addText(fmtChg(it.chg));\n    ct.font = Font.boldMonospacedSystemFont(L.fChg);\n    ct.textColor = c.fg;\n  }\n}\n\nfunction buildWidget(data) {\n  const w = new ListWidget();\n  // 深色下纯黑背景：OLED 屏常亮/待机展示时更省电\n  w.backgroundColor = Color.dynamic(new Color(\"#ffffff\"), new Color(\"#000000\"));\n  // 在 app 里手动运行时 widgetFamily 是 null——按小号渲染，和桌面上实际用的一致\n  const family = config.widgetFamily ?? \"small\";\n  const large = family === \"large\";\n  const pad = large ? 18 : 14;\n  w.setPadding(pad, pad + 2, pad - 2, pad + 2);\n  if (PANEL_URL) w.url = PANEL_URL;\n\n  const requested = Array.isArray(data.requested) && data.requested.length ? data.requested : data.items;\n  const hasStocks = requested.some((it) => it.type === \"stock\");\n  const hasCoins = requested.some((it) => it.type === \"coin\");\n  const market = hasStocks ? usMarketStatus() : { label: \"24H\", open: true };\n  const mins = market.open ? REFRESH_OPEN_MIN : REFRESH_CLOSED_MIN;\n  w.refreshAfterDate = new Date(Date.now() + mins * 60e3);\n\n  const head = w.addStack();\n  head.centerAlignContent();\n  const titleText = family === \"small\"\n    ? (hasStocks ? \"行情\" : \"币价\")\n    : [hasStocks ? \"美股\" : null, hasCoins ? \"Crypto\" : null, market.label].filter(Boolean).join(\" · \");\n  const title = head.addText(titleText);\n  title.font = Font.semiboldSystemFont(large ? 12 : 10);\n  title.textColor = SECONDARY;\n  head.addSpacer();\n  const t = new Date(data.ts);\n  const stamp = head.addText(\n    (family === \"small\" ? market.label + \" \" : \"\") +\n      pad2(t.getHours()) + \":\" + pad2(t.getMinutes()) +\n      (data.note ? \" · \" + data.note : \"\")\n  );\n  stamp.font = Font.systemFont(large ? 12 : 10);\n  stamp.textColor = data.note ? WARN : SECONDARY;\n  w.addSpacer(large ? 10 : 7);\n\n  const L = layoutFor(family, data.items.length);\n  const columns = splitColumns(data.items, L);\n  const body = w.addStack();\n  body.topAlignContent();\n  // 小号单列：行间距 = 固定最小值 + 弹性均摊，把剩余高度分散到行间，\n  // 消掉底部大片留白。双列布局保持固定行距 + 顶部对齐，保证两列行行对齐。\n  const flexFill = family === \"small\";\n  columns.forEach((list, i) => {\n    if (i > 0) body.addSpacer(large ? 26 : 18);\n    const col = body.addStack();\n    col.layoutVertically();\n    list.forEach((it, j) => {\n      if (j > 0) {\n        col.addSpacer(L.gap);\n        if (flexFill) col.addSpacer();\n      }\n      addRow(col, it, L);\n    });\n    if (!flexFill) col.addSpacer();\n  });\n  return w;\n}\n\nfunction buildErrorWidget(msg) {\n  const w = new ListWidget();\n  w.backgroundColor = Color.dynamic(new Color(\"#ffffff\"), new Color(\"#000000\"));\n  w.setPadding(16, 16, 16, 16);\n  w.refreshAfterDate = new Date(Date.now() + 15 * 60e3);\n  if (PANEL_URL) w.url = PANEL_URL;\n  const t1 = w.addText(\"行情加载失败\");\n  t1.font = Font.semiboldSystemFont(13);\n  t1.textColor = PRIMARY;\n  w.addSpacer(6);\n  const t2 = w.addText(String(msg).slice(0, 80));\n  t2.font = Font.systemFont(11);\n  t2.textColor = SECONDARY;\n  w.addSpacer(6);\n  const t3 = w.addText(\n    WORKER_URL\n      ? \"检查网络后等待系统刷新；点击小组件可打开配置页\"\n      : \"直连模式可把 STOCK_SOURCE 改为 \\\"tencent\\\"\"\n  );\n  t3.font = Font.systemFont(10);\n  t3.textColor = SECONDARY;\n  return w;\n}\n\n// ---------- 主流程 ----------\nlet widget;\ntry {\n  widget = buildWidget(await getData());\n} catch (e) {\n  widget = buildErrorWidget((e && e.message) || e);\n}\nScript.setWidget(widget);\nif (!config.runsInWidget) {\n  if (config.widgetFamily === \"medium\") await widget.presentMedium();\n  else if (config.widgetFamily === \"large\") await widget.presentLarge();\n  else await widget.presentSmall();\n}\nScript.complete();\n",
  "share_sheet_inputs": []
}