phone-lens 1.1.3 → 1.1.5

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
@@ -2,7 +2,24 @@
2
2
 
3
3
  版本号三处联动:host `package.json` ↔ App `pubspec.yaml`(三段 = tag,build number 只增不减)↓ Release tag。App 版本号必须与 Gitee release tag 同步 bump,否则手机端会反复弹"发现新版本"。
4
4
 
5
- ## 1.1.3(未发布)
5
+ ## 1.1.5(2026-11-07)
6
+
7
+ - **新增:裁剪页精细编辑(App 端)**。画笔/马赛克模式下双指捏合缩放 + 拖动平移整幅画面(1x~6x,以双指中心为锚点),放大后可做精细标注;右上角出现「回原比例」小按钮一键复位。缩放只是视图变换,笔画/裁剪框数据全部保持在图像空间,导出结果与屏幕所见严格一致,与裁剪、画笔、马赛克、旋转互不干扰(旋转/翻页自动复位缩放)。
8
+ - **修复:双指缩放残留误触笔画点**。双指落下时自动清扫 600ms 内未延伸的单点笔画(缩放前每根手指的落点都会留下一个小点),有意画的单指点不受影响。
9
+ - **改进:次级工具栏可收起**。画笔/马赛克激活后再次点击主工具栏按钮收起参数面板(工具保持激活),不再遮挡画面编辑区域;切换到其他工具时自动重新展开。
10
+ - **改进:笔画大小改滑条连续调节 + 六色画笔**。三档圆点按钮换成滑条(画笔宽度约图像宽度 0.4%~8%、马赛克块 1%~12% 拖动即见),新增红/蓝/黄/绿/黑/白色板,颜色贯通预览与最终导出(原固定红色)。
11
+ - **改进:上传画质四档 → 五档**:原图(原始尺寸)/ 高(2048px 超精细细节)/ 中(1280px,**默认改为中档**)/ 低(960px,约 130KB)/ 极低(320px 细节不可辨认);已保存的旧档位选择自动按新参数生效,无需重新设置。
12
+ - **修复:有标注内容时旋转直接清空无提示**——现在先弹「清除标注并旋转?」确认框,空画面旋转不弹窗。
13
+ - **修复:Gitee `releases/latest` 端点"最近编辑"陷阱**(双端三处:App `update_check`、host `apk-link`、host `update-check` 改用 releases 列表端点 + 客户端版本比较取最高 tag)。此前在 Gitee 网页上编辑/改名任何老发行版都会让 App 误判"已是最新版本"、二维码指向旧 APK,三处全部中招;现摆脱对该端点语义的依赖。
14
+
15
+ ## 1.1.4(2026-11-03)
16
+
17
+ - **新增:推流画质四档可调(App 端)**。设置 → 拍摄与上传 → 推流画质:极低(320P 8fps)/ 低(480P 10fps)/ 中(默认,640P 10fps,与升级前取景画质一致,跟随电脑端下发参数)/ 高(720P 10fps)。仅影响电脑端实时取景,不影响拍摄照片画质(照片仍由"上传画质"决定);切换下一帧即生效,无需重启预览或重连;帧协议不变(裸 JPEG 二进制),host 与 Web UI 零改动,新老版本双向兼容,无需版本门槛。推流画质存独立档位键,不被电脑端下发的预览参数在重连时覆盖("中"档则严格采用下发值)。
18
+ - **修复:会话追踪数据面与宿主真实状态脱节(热重载补漏)**。`agent/created` 只在 agent 创建瞬间派发一次(无回放,cordis 事件无 retain),插件(重)加载晚于会话创建时内部存活表/活跃目标为空。现在声明 `agents` 服务依赖,服务就绪后枚举 `ctx.agents.list()` 回填:运行中的会话立即成为投递目标(与 `agent/status`→running 同语义),空闲会话进入存活表(pinned 目标模式可解析);无 agents 服务的环境(standalone/dev)自动跳过,任何回填异常都不影响接收服务。
19
+ - devDependencies 对齐宿主实际运行版本并钉死精确版本(cordis 4.0.5、dsh-llm 0.2.1-rc.4、dsh-tools 0.2.1-rc.4、schemastery 3.18.5),类型参考与 0.2.1 一致;peerDependencies 范围不变。
20
+ - 文档勘误(docs/dsh-caps.md、docs/architecture.md):移除对 0.1.7 已删除 `agent/session-start` 事件的引用;补记 live ctx 事件(`dsh-agent-loop` 派发)与持久会话日志事件(`agent/inbox/spliced`)的两层模型,防止按单包检索误判事件死活。
21
+
22
+ ## 1.1.3(2026-10-16)
6
23
 
7
24
  - **新增:前后置镜头切换(双端)**。手机端取景器右上角新增镜头切换按钮(单摄设备自动隐藏,切换中转圈);电脑端新增模型工具 `phone_camera_lens`——`lens` 传 "front"/"back" 切换镜头,不带参数查询当前镜头与设备可用镜头(工具对相机关闭的手机同样可用:仅记录偏好,下次开启生效)。需手机端 ≥ 1.1.3,沿用三档失败判定。
8
25
  - **精简:`phone_camera_zoom_info` 并入 `phone_camera_zoom`**——不带参数调用 zoom 即查询当前倍数与支持范围(手机端早已支持,纯工具层冗余);工具总数 7→6。`phone_camera_pause`/`resume` 审查后保留独立(反义动作直命名对模型更清晰)。
package/lib/dev.js CHANGED
@@ -399,7 +399,7 @@ const ERROR_CODES = {
399
399
 
400
400
  //#endregion
401
401
  //#region src/server/update-check.ts
402
- const GITEE_LATEST_API$1 = "https://gitee.com/api/v5/repos/qianfengbingtang/phone-lens/releases/latest";
402
+ const GITEE_RELEASES_API$1 = "https://gitee.com/api/v5/repos/qianfengbingtang/phone-lens/releases?per_page=10&direction=desc";
403
403
  /** A fresh result suppresses further API calls for 12h (host sits open all day). */
404
404
  const OK_TTL_MS = 12 * 60 * 60 * 1e3;
405
405
  /** A failed check retries after 30min instead of hammering the API per panel-open. */
@@ -431,15 +431,25 @@ function compareVersions(a, b) {
431
431
  }
432
432
  return 0;
433
433
  }
434
- /** Latest release tag from the trusted Gitee feed, or null on any failure. */
434
+ /** Highest release tag across the first page of the trusted Gitee feed, or
435
+ * null on any failure. Client-side max() over tag_name, so a stale-but-recently
436
+ * edited release can never shadow the real newest version. */
435
437
  async function fetchLatestGiteeTag() {
436
438
  try {
437
- const url = new URL(GITEE_LATEST_API$1);
439
+ const url = new URL(GITEE_RELEASES_API$1);
438
440
  if (url.protocol !== "https:" || url.host !== "gitee.com") return null;
439
441
  const res = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
440
442
  if (!res.ok) return null;
441
443
  const data = await res.json();
442
- return typeof data.tag_name === "string" && data.tag_name.trim() ? data.tag_name.trim() : null;
444
+ if (!Array.isArray(data)) return null;
445
+ let best = null;
446
+ for (const row of data) {
447
+ const tag = row?.tag_name;
448
+ if (typeof tag !== "string" || !tag.trim()) continue;
449
+ const clean = tag.trim();
450
+ if (best === null || compareVersions(clean, best) > 0) best = clean;
451
+ }
452
+ return best;
443
453
  } catch {
444
454
  return null;
445
455
  }
@@ -524,7 +534,7 @@ async function admitImage(input, attachments, fallbackDir) {
524
534
 
525
535
  //#endregion
526
536
  //#region src/server/apk-link.ts
527
- const GITEE_LATEST_API = "https://gitee.com/api/v5/repos/qianfengbingtang/phone-lens/releases/latest";
537
+ const GITEE_RELEASES_API = "https://gitee.com/api/v5/repos/qianfengbingtang/phone-lens/releases?per_page=10&direction=desc";
528
538
  const CACHE_TTL_MS = 10 * 60 * 1e3;
529
539
  const REQUEST_TIMEOUT_MS = 6e3;
530
540
  let cached = null;
@@ -545,15 +555,17 @@ async function fetchLatestGiteeApk() {
545
555
  const ctrl = new AbortController();
546
556
  const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
547
557
  try {
548
- const resp = await fetch(GITEE_LATEST_API, {
558
+ const resp = await fetch(GITEE_RELEASES_API, {
549
559
  signal: ctrl.signal,
550
560
  headers: { accept: "application/json" }
551
561
  });
552
562
  if (!resp.ok) return null;
553
563
  const data = await resp.json();
554
- const assets = data?.assets;
555
- if (!Array.isArray(assets)) return null;
556
- for (const asset of assets) {
564
+ if (!Array.isArray(data)) return null;
565
+ const rows = data.filter((row) => typeof row?.tag_name === "string" && !!row.tag_name.trim()).sort((a, b) => compareVersions(b.tag_name.trim(), a.tag_name.trim()));
566
+ const newest = rows[0];
567
+ if (!newest || !Array.isArray(newest.assets)) return null;
568
+ for (const asset of newest.assets) {
557
569
  const row = asset;
558
570
  if (typeof row.name === "string" && row.name.startsWith("app-release.apk") && isTrustedGiteeAsset(row.browser_download_url)) return row.browser_download_url;
559
571
  }
package/lib/index.d.ts CHANGED
@@ -18,7 +18,7 @@ import z from "@deepseek-ai/schemastery";
18
18
  */
19
19
  declare class PhoneLens extends Service {
20
20
  static inject: string[];
21
- static Config: z<any, any>;
21
+ static Config: z<any, any, "plain">;
22
22
  constructor(ctx: any, rawConfig: unknown);
23
23
  }
24
24
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":null,"mappings":";;;;;;;;;;;;;;;;;;cA0BqB,SAAA,SAAkB,OAAA;;EAAlB,OAAA,MAAU,EAUhB,CAVgB,CAAA,GAAA,EAAA,GAAA,CAAA;EAAA,WAAA,CAAA,GAAA,EAAA,GAAA,EAAA,SAAA,EAAA,OAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":null,"mappings":";;;;;;;;;;;;;;;;;;cA0BqB,SAAA,SAAkB,OAAA;;EAAlB,OAAA,MAAU,EAUhB,CAVgB,CAAA,GAAA,EAAA,GAAA,EAAA,OAAA,CAAA;EAAA,WAAA,CAAA,GAAA,EAAA,GAAA,EAAA,SAAA,EAAA,OAAA"}
package/lib/index.js CHANGED
@@ -121,6 +121,21 @@ var HostDeliverySink = class {
121
121
  this.lastActiveId = id;
122
122
  this.log("info", `tracking agent ${id} (active target)`);
123
123
  }
124
+ /**
125
+
126
+ * Record an agent as alive WITHOUT promoting it to the active target —
127
+
128
+ * hot-reload backfill only. A *running* agent claims the target via track();
129
+
130
+ * idle ones land here so pinned-mode delivery still resolves against the
131
+
132
+ * alive-map instead of reporting "no live agent".
133
+
134
+ */
135
+ trackIdle(agent) {
136
+ const id = agent.id ?? String(agent.session.id);
137
+ this.agents.set(id, agent);
138
+ }
124
139
  /** Forget a disposed agent. */
125
140
  untrack(agent) {
126
141
  const id = agent.id ?? String(agent.session.id);
@@ -467,7 +482,7 @@ const ERROR_CODES = {
467
482
 
468
483
  //#endregion
469
484
  //#region src/server/update-check.ts
470
- const GITEE_LATEST_API$1 = "https://gitee.com/api/v5/repos/qianfengbingtang/phone-lens/releases/latest";
485
+ const GITEE_RELEASES_API$1 = "https://gitee.com/api/v5/repos/qianfengbingtang/phone-lens/releases?per_page=10&direction=desc";
471
486
  /** A fresh result suppresses further API calls for 12h (host sits open all day). */
472
487
  const OK_TTL_MS = 12 * 60 * 60 * 1e3;
473
488
  /** A failed check retries after 30min instead of hammering the API per panel-open. */
@@ -499,15 +514,25 @@ function compareVersions(a, b) {
499
514
  }
500
515
  return 0;
501
516
  }
502
- /** Latest release tag from the trusted Gitee feed, or null on any failure. */
517
+ /** Highest release tag across the first page of the trusted Gitee feed, or
518
+ * null on any failure. Client-side max() over tag_name, so a stale-but-recently
519
+ * edited release can never shadow the real newest version. */
503
520
  async function fetchLatestGiteeTag() {
504
521
  try {
505
- const url = new URL(GITEE_LATEST_API$1);
522
+ const url = new URL(GITEE_RELEASES_API$1);
506
523
  if (url.protocol !== "https:" || url.host !== "gitee.com") return null;
507
524
  const res = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
508
525
  if (!res.ok) return null;
509
526
  const data = await res.json();
510
- return typeof data.tag_name === "string" && data.tag_name.trim() ? data.tag_name.trim() : null;
527
+ if (!Array.isArray(data)) return null;
528
+ let best = null;
529
+ for (const row of data) {
530
+ const tag = row?.tag_name;
531
+ if (typeof tag !== "string" || !tag.trim()) continue;
532
+ const clean = tag.trim();
533
+ if (best === null || compareVersions(clean, best) > 0) best = clean;
534
+ }
535
+ return best;
511
536
  } catch {
512
537
  return null;
513
538
  }
@@ -592,7 +617,7 @@ async function admitImage(input, attachments, fallbackDir) {
592
617
 
593
618
  //#endregion
594
619
  //#region src/server/apk-link.ts
595
- const GITEE_LATEST_API = "https://gitee.com/api/v5/repos/qianfengbingtang/phone-lens/releases/latest";
620
+ const GITEE_RELEASES_API = "https://gitee.com/api/v5/repos/qianfengbingtang/phone-lens/releases?per_page=10&direction=desc";
596
621
  const CACHE_TTL_MS = 10 * 60 * 1e3;
597
622
  const REQUEST_TIMEOUT_MS = 6e3;
598
623
  let cached = null;
@@ -613,15 +638,17 @@ async function fetchLatestGiteeApk() {
613
638
  const ctrl = new AbortController();
614
639
  const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
615
640
  try {
616
- const resp = await fetch(GITEE_LATEST_API, {
641
+ const resp = await fetch(GITEE_RELEASES_API, {
617
642
  signal: ctrl.signal,
618
643
  headers: { accept: "application/json" }
619
644
  });
620
645
  if (!resp.ok) return null;
621
646
  const data = await resp.json();
622
- const assets = data?.assets;
623
- if (!Array.isArray(assets)) return null;
624
- for (const asset of assets) {
647
+ if (!Array.isArray(data)) return null;
648
+ const rows = data.filter((row) => typeof row?.tag_name === "string" && !!row.tag_name.trim()).sort((a, b) => compareVersions(b.tag_name.trim(), a.tag_name.trim()));
649
+ const newest = rows[0];
650
+ if (!newest || !Array.isArray(newest.assets)) return null;
651
+ for (const asset of newest.assets) {
625
652
  const row = asset;
626
653
  if (typeof row.name === "string" && row.name.startsWith("app-release.apk") && isTrustedGiteeAsset(row.browser_download_url)) return row.browser_download_url;
627
654
  }
@@ -2723,6 +2750,32 @@ var PhoneLens = class extends Service {
2723
2750
  if (payload.status === "running") onAgent(payload.agent);
2724
2751
  });
2725
2752
  ctx.on?.("agent/disposed", (payload) => offAgent(payload.agent));
2753
+ let backfilled = false;
2754
+ const runBackfill = (registry) => {
2755
+ if (backfilled) return;
2756
+ if (!registry) try {
2757
+ registry = ctx.agents;
2758
+ } catch {
2759
+ return;
2760
+ }
2761
+ if (!registry || typeof registry.list !== "function") return;
2762
+ const live = registry.list() ?? [];
2763
+ let running = 0;
2764
+ for (const agent of live) if (agent?.status === "running") {
2765
+ running++;
2766
+ onAgent(agent);
2767
+ } else sink.trackIdle(agent);
2768
+ backfilled = true;
2769
+ if (live.length > 0) log("info", `backfilled ${live.length} live agent(s) (${running} running) from ctx.agents`);
2770
+ };
2771
+ ctx.inject?.(["agents"], (injectCtx) => {
2772
+ try {
2773
+ runBackfill(injectCtx.agents);
2774
+ } catch {}
2775
+ });
2776
+ setTimeout(() => {
2777
+ if (!backfilled) runBackfill();
2778
+ }, 0).unref?.();
2726
2779
  try {
2727
2780
  const PHONE_TOOLS = new Set([
2728
2781
  "phone_take_photo",