cortico-world-presence
v0.3.2
Published
Cortico World: 在场感(Windows 专用)——能判别你是否离开/回到电脑前。★ 它会持续读前台窗口标题与键鼠空闲、并能截屏(屏幕上有什么截到什么),这些随模型调用发到你部署配置的模型端点;默认不启用。详细隐私说明见 README。 ★ 支持 Coopanion(Coo):已在 Coopanion 0.2.5 上装机验证;出问题可去 GitHub 提 issue,或到 QQ 群里 @筆離嘢(与作者名同名)。
Readme
cortico-world-presence
要求 Cortico 0.2.0 及以上(扩展契约 world 6,
cortico.api: 6)。 在 Cortico 0.1.x 上装它不会被加载。这一条是 0.2.0 起的要求 —— 0.1.x 那一版要求旧契约。
Cortico 的「在场感」World:它能判别你是否离开 / 回到电脑前,也知道你正在用哪个窗口,想看的时候能截一张屏。 Windows 专用(传感器与截图都走 Win32,经由系统自带的 PowerShell,零安装)。
★ 隐私提示(从扩展市场装上之前先读这段)
它会给 bot 三样东西:
- 你在不在电脑前 ——
presence.away/presence.back就是「你离开了多久 / 你回来了」, 它读的是系统给的「距上次键鼠输入多少毫秒」,不记录你按了什么键、点了什么(没有键盘钩子);- 你在用哪个窗口 —— 前台窗口的进程名与完整标题,持续采样(标题里常有文件名、网页标题、 聊天对象的名字);
- 屏幕本身 —— bot 想看的任何时候都能截屏:整屏、任意区域、任意窗口都行, 屏幕上有什么就截到什么(私聊窗口、邮件、密码管理器、视频画面一样截)。
这些东西去哪:
- 本包自己不联网 —— 没有任何出站请求,我们这边也没有服务器;
- 窗口标题、事件正文与截图是随模型调用发出去的,实际到达的地方就是你部署里配的那个 模型端点(你选的那个模型服务商,以及它的留存策略);
- 截图另会按内容哈希存进
<部署>/data/media/,并在被上下文裁剪掉之前每次请求都重发一遍。换句话说:开了它,等于把屏幕交给 bot 看,而这段内容要经过你选的模型服务商。
enabled默认false—— 装上不会自己开始采样。开之前请确认两件:①你能接受屏幕上出现的东西被发出去; ②你信得过那个端点的留存策略。想收紧就靠enabled、pollIntervalMs,以及不在不该开的时候开着 (它没有「只截某个程序」「模糊标题」这类旋钮,这是刻意的:本包不做隐私开关,是开发者的拍板,不是漏做)。
在 Coopanion(Coo)上使用
已在 Coopanion 0.2.5(Windows x64 便携版)上装机验收:市场里检查与安装成功,重启后导入成功;
启用后传感器 online,presence_windows 的空闲值与前台窗口采样都有效。默认 digest 挡首帧只打底,
不会立刻投事件 —— 想看到东西要先压到 1 分钟或调一次 presence_windows。这一次没有单独测截图。
Coo 的扩展市场默认可能走镜像源(npmmirror),镜像上偶尔还没有新版本 —— 检查时报「registry 没有给出
cortico-world-presence 的 x.y.z 版本」,就把 Coo 的扩展源改成官方 https://registry.npmjs.org 再装。
装完或改过配置要整进程重启 Coo 才生效。
遇到问题:去 GitHub 开 issue —— https://github.com/biliye/cortico-world-presence/issues; 或者到 QQ 群里 @筆離嘢(和作者名同名)。附上 Coo 版本、本包版本和报错截图,再说清是哪一步出的。
怎么装
Windows 专用,零安装 —— 传感器与截图都走系统自带的 PowerShell,不用装任何别的东西。
- 装进部署:控制台「扩展 → 扩展市场」里找到它点安装(它在部署的
extensions/里跑一次pnpm add);或手动cd extensions && corepack pnpm add --ignore-workspace cortico-world-presence。 ★ 若本机默认源是镜像(npmmirror),装的时候显式指官方源--registry=https://registry.npmjs.org/。 - 在部署的
config.json里写worlds.presence(至少enabled: true,其余见下面的配置表), 重启实例。 - 重启后挂的是
digest挡 —— 窗口变化只记账,每 20 分钟折成一条presence.digest。想看效果先让她presence_set_mode {mode:"digest", minutes:1}压到 1 分钟,再在几个窗口之间来回切。
enabled 默认 false:装上去不会自己开始采样。
它给什么
- 事件(被动):
presence.switched—— 前台窗口切到了哪(应用名 + 标题 + 上一个窗口停留多久);presence.activity—— ambient 姿势下的合并批,或 call 姿势下 3 秒内的连续变化;presence.away/presence.back—— 键鼠空闲超过阈值 / 恢复输入,带离开时长(从最后一次键鼠输入 算起,不是从「发现离开」算起);presence.digest—— 压缩挡下每周期一份累计摘要:应用/窗口停留总时长、观测切换次数、最近状态; 原始逐段时间线按需查询,无切换仍计持续停留;presence.down—— 传感器进程起不来或退了。
- 工具(主动):
presence_windows—— 前台、键鼠空闲秒数、屏幕几何(虚拟屏 + 每台显示器)、每个可见窗口的 矩形;默认附活动摘要(压缩挡下这就是「结账」:当场给累计视图、周期重开)。view:"timeline"按记录编号分页读取原始时间线,不列当前窗口、不清账、不重开周期;presence_screenshot—— 截图。三种瞄准方式,优先级:x/y/width/height(任意矩形区域)>hwnd(整个窗口)>target(foreground默认 /full);PNG 或 JPEG 直接附进回执, 模型不吃图时回执里会把话说破。grid:true会在图上叠一层淡格线(每条线旁标着它自己的桌面坐标); 回执里始终写着「图上 1px = 桌面多少 px」与图左上角对应的桌面点;presence_set_mode—— 切四挡投递姿势(见下);运行态,重启回默认的 digest。
四挡投递姿势(2026-10-13 加的后两挡,并把默认从 ambient 换成了 digest)
| 挡 | 窗口变化 | 离开 | 回来 | 叫醒 |
|---|---|---|---|---|
| digest(压缩,默认) | 只记账,每周期(默认 20 分钟)投一份累计摘要 | 投 + 记进摘要 | 投 + 记进摘要 | 只有「回来」,且要离开够久 |
| ambient | 攒成批、合并投出 | 单独事件,随下轮送达 | 单独事件,随下轮送达 | 不叫醒 |
| call | 每次变化当场投(3 秒合并) | 单独事件,随下轮送达 | 当场投 | 窗口变化;「回来」要离开够久 |
| off(关闭) | 只留档、不主动投 | 当场投 | 当场投 | 只有「回来」(要够久);切到「本次运行第一次见到的程序」报一行但不叫醒 |
「离开」在任何挡下都不叫醒(2026-10-13 他定的)——他离开时她多半没什么可做的。事件照投, 只是随她下次开口一起到(即 call 挡下窗口变化仍叫醒、只有「离开」这一条不叫醒)。
「回来」要离开够久才叫醒(2026-10-14 他定的):离开超过
backWakeAfterMs(默认 10 分钟) 才当场叫醒,更短的只 piggyback、随她下次开口一起到——免得为「他去倒杯水」打断她。设0就 回到「每次回来都叫醒」。ambient 挡一直不叫醒。两个时长都从「最后一次键鼠输入」算起(2026-10-14 修):
presence.away正文写明最后一次输入在 几点,presence.back报的是真实离开时长,摘要里的「离开键盘累计」同样。注意事件到达仍然比 真正离开晚awayAfterMs——那是阈值检测的固有代价,改的是它说的是几点。压缩挡默认列累计时长、切换次数与最近状态。周期用
presence_set_mode {mode:"digest", minutes:5}可以临时改;想立刻要一份就调presence_windows(结账,当场给、周期重开)。精度就是轮询粒度,周期末尾还开着的那个窗口 只能算到结账那一刻;下周期从结账时刻继续累计,不重复算前一周期。关闭挡是投递静音,不是关掉这个 World:传感器照采、两个工具照用;跟配置里的
enabled:false(整个 World 不启动)是两码事。「没见过」按程序名算、只在本次运行内有效——重启后前几个 窗口会再被当成新的报一次。离开/回来在任何挡下都投自己的事件名(
presence.away/presence.back),不再混进窗口变化的 批里(从前单独一条会被错标成presence.switched,顺手修掉)。「离开」不叫醒、「回来」要达到 门槛才叫醒(见上两条)。
累计摘要与按需时间线(2026-10-14)
digest 的自动事件和 digest/off 下默认的 presence_windows 结账都给紧凑视图:
- 应用按进程名(忽略大小写)累计;窗口按应用、句柄和完整标题累计,标题变化另算窗口项。
- 各列时长前 5 项,其余报项数与合计;展示标题截短,原始时间线保留完整标题。
- 切换次数是本周期观测到的窗口/标题变化,不用“时段数减一”估算;首次采样、重复采样、结账不算切换。
- 没切换也有持续时长。前台停留包含键鼠空闲,不等于工作时长;离开键盘另计,与窗口时段可能重叠。 尚未回来也会按结账时刻分段,回来后不会重复算上一周期。前台采样为 null 时停止累计旧窗口。 离开段的起点是最后一次键鼠输入那一刻(不再是「发现离开」那一刻)。
每份摘要附记录编号。查询旧记录不会调用桌面采样、清账或重置计时器:
{"view":"timeline","snapshot":"摘要中的编号","offset":0,"limit":40}以上参数传给 presence_windows。snapshot 省略取最近一次结账;offset 从 0 起,limit 默认 40、
范围 1–100;回执给下一页参数。原始时间线按起始时间排序,记录编号在新周期产生后仍稳定。
仅保留最近 8 次有效结账,每份最多 400 段,运行态、重启即忘;编号过期明确失败。
账本超限时会报告丢弃段数,累计时长与时间线只覆盖保留段,切换次数仍覆盖整个周期。
off 切到其他挡时不主动发送摘要,但保留这一份时间线供查询。ambient/call 的投递方式维持原状。
坐标与区域
坐标一律是物理像素、虚拟屏坐标(主屏左上为原点,主屏左边的显示器是负坐标)。两个脚本都声明 DPI 感知,所以窗口矩形与区域坐标同一坐标系,可以直接用来框。
- 框到屏幕外会被夹回虚拟屏内,回执里说明实际抓了哪一块;完全不重叠则报错并给出屏幕范围。
- 区域长边超过 1600 会被等比压小,回执里会提醒「想看清小字就把框框小一点」,并写明「图上 1px = 桌面多少 px」与图左上角对应的桌面点——小框不走降采样,按原始像素给,这是「读屏幕上的小字」 的唯一办法;
grid:true会在图上叠一层淡格线,每条线落在桌面坐标的整百处、线旁标着那个坐标;被压过的大图 上间距自动翻倍(2560 宽压到 1600 时 100→200 桌面像素),回执里报出实际间距与条数。要按图上的 位置去点什么东西时加它;- 区域截的是屏幕上那一块的当前样子(遮挡也算),与
hwnd的PrintWindow不同。 - 盯同一块看变化:World 记住每块区域上次的 sha256,回执直接给「和上次一样(像素级完全相同)」
或「有变化」。运行态、重启即忘。时钟/视频这类常动的东西会让它永远显示有变化,环境提示词里说了;
图上那个鼠标箭头也算进去(它是我们画的),所以光标挪进框里来过一次,这块就「有变化」——
真盯着一块看的时候用
cursor:false,那条「和上次一样」才说得出口(见下)。
图上的鼠标(2026-10-07 加的)
抓屏本身拍不到光标——量过:围着光标框一张 PNG,图里没有箭头。所以图上的箭头是我们照着系统的
真光标画上去的(GetCursorInfo 拿位置与位图,DrawIconEx 画在降采样之后的图上)。
几条分寸:
- 画的是系统真光标那一张位图,所以形状是如实的:箭头=普通地方、I 形=能打字的地方、沙漏=窗口忙。 这比一个固定的箭头图标多一层信息(「这一点上是什么」),代价是形状随系统主题变。
- 跟着图一起缩:按这块画面被压的比例缩,但最小 16 像素——被压过的大图上它不会小成一个点。
- 只画在真抓屏的图上。
hwnd那条走PrintWindow(画面是窗口自己绘的帧,可能被别的窗口盖着、 光标根本不在上面),那种一律不画,并在回执里说清是这条原因。抓屏那条(全屏/区域/前台窗口)照画。 - 系统把光标藏起来时也不画(全屏视频、某些程序会自己隐藏),如实说。
- 不在这张图里就不画,并说出图覆盖的是哪一块——不然她会把「没有箭头」读成「光标不在这块」 或者「这个功能坏了」。
- 回执里始终给出光标的桌面坐标(虚拟屏物理像素),不管画没画。图上有箭头 + 正文里有数字, 两样一起她才既看得见又量得准。
- 采样在抓屏那一刻之前读一次(帧是瞬时的,越靠近它越准),不是每张图各读一遍。
她可以不要那个箭头(cursor:false,默认画):位置照读照报,可选的只有图上那支箭头,
所以关掉它不会让她丢任何有用的信息。要它的场合是两处:
- 盯同一块看变化(反复截同一块,回执说「和上次像素级完全相同」还是「有变化」)——她刚用鼠标 点完某处,光标常常正好停在她要盯的那一块里,一个会动的箭头会让「完全相同」永远说不出口;
- 读那一块的小字——二十个像素的箭头能盖掉一个字(实测:240×120 那一块里,箭头压着的那行字 在关掉之后才读得全)。
为什么不做成部署配置里的开关:这不是「他的偏好」而是「这一次要看什么」——同一张工具在两种用途
之间来回,所以它按包内惯例做成每次调用一个布尔(与 grid 同一个形状),而不是一个在 config 里
改完要重启的档位。少一个旋钮,也少一处「改完得重启才生效」。
截图的成本与临时性
图是真的进上下文(ToolOutcome.blobs),不是只留档:框架把它按内容哈希存进
<部署>/data/media/,同时每次请求都从那个文件读回来、以 data URL 重发一遍,直到这条回执
被上下文裁剪挤出去。持续多久取决于部署的上下文预算与裁剪策略;把观察写成文字不会删除原图。
因此三条经济性设定:
- 默认只截前台窗口,不再默认全屏——少暴露、少像素;
- 照片类画面自动转 JPEG(判定:PNG 字节数 ÷ 像素数 > 0.5;实测文字界面 0.10、视频窗口 1.21), 体积差一个数量级;文字界面保持 PNG,字才清楚;
- 回执里明说图是临时的,并请她把要紧的内容写成文字;第 2 张起还会提一句
presence_windows更省。想接着看他在干什么,反复查窗口列表比反复截图划算得多。
框架侧目前没有让工具回执照「只用一轮」的机制(ephemeral 只接了事件,没接工具回执),
所以「图一直躺在上下文里」这件事从 World 里解决不了,只能这么压着。值得给 Cortico 提。
配置(worlds.presence)
| 键 | 默认 | 说明 |
|---|---|---|
| enabled | false | bot 或部署启用 |
| pollIntervalMs | 10000 | 轮询间隔;两次轮询之间的瞬时变化看不见。下限 2000,改完要重启 World |
| awayAfterMs | 300000 | 键鼠空闲超过这么久算「离开」。热生效 |
| backWakeAfterMs | 600000 | 「回来」值得当场叫醒的离开时长;更短的只随她下次开口一起到。0 = 每次回来都叫醒。热生效。要比 awayAfterMs 长才有意义 |
没有密钥。窗口标题与截图会随模型调用发到部署配置的模型端点——这是设计内的事,部署者知情; 不加隐私旋钮是开发者的拍板(2026-09-19)。
验证
pnpm typecheck
pnpm test # 单元测试用注入的假传感器;另有一条真 PowerShell 的采样用例
pnpm smoke # 对真桌面:采样 6 秒 + 列窗口 + 真截全屏、一块区域、光标周围那一块(都落盘)pnpm test 里有一条独立验证:截图前后各另起一个进程、用另一条 API(GetCursorPos)量一次光标,
被测代码报出来的那个点必须落在两次之间。这挡的是「写死的坐标」「恒为 0」「被 DPI 虚拟化过的值」
这三类假绿——它们都过不了这一关。
pnpm smoke 会把光标周围那一块 1:1 的图落盘(presence-smoke-cursor.png),那是「箭头到底画
上没有、位置对不对」唯一便宜的验法:看一眼。像素级的自动判据在这里都不便宜(系统光标形状随主题
变,写死模板就是给以后埋一条假红),所以这一步是有意留给眼睛的。
一条要记住的预算(2026-10-07 撞上的):captureScript 生成的脚本是编码成一行塞在命令行上跑的
(-EncodedCommand,UTF-16 再 base64 ⇒ 源文本的 2.67 倍),而 Windows 的命令行约 32767 字符。
顶过去的表现是 spawn ENAMETOOLONG——一条完全看不出「你多写了几行注释」的报错。所以:
写进那个模板的每个字都按 2.67 倍算钱,解释写在 TypeScript 这一侧;capture.test.ts 里有一条
用例盯着这个数,离上限不足 2048 字节就当场红(比运行时炸好认)。不改成「写临时 .ps1 用 -File 跑」的
理由:脚本里有中文文案(给她的错误说明),无 BOM 的 UTF-8 会被 PowerShell 5.1 按 ANSI 读成乱码。
装进实例后的第三级目击:默认 digest 挡等待 presence.digest,call 挡可观察 presence.switched/activity;让她调 presence_windows 与
presence_screenshot;重点:她拿到截图后能否真的描述出屏幕内容(模型端吃不吃图的实底,
框架侧 multimodal 只保证 PNG 附进了请求)。
另外让她截一张别的窗口(长边超过 1600 的那种)并加 grid:true:回执里该出现「图上 1px = 桌面
多少 px」那句,图上该出现带坐标标注的格线。
光标那一条:她截一张全屏——现象:图上有个系统样式的箭头,回执里那一句写着光标在桌面哪个坐标;
这时你动一下鼠标再让她截一张,箭头应该跟着挪过去了(那一下「跟着挪」就是它真的在读光标的证据)。
关掉它那一条:让她 {"cursor":false} 截同一块——现象:图上没有箭头,而回执里照样写着光标在
哪个坐标,并说清是「你说了这次不要箭头」。
四挡那两条(2026-10-13 加的;默认挡就是 digest):重启后不用切、挂着的就是压缩挡,只是周期是
20 分钟——要看效果先让她 presence_set_mode {mode:"digest", minutes:1} 压到 1 分钟,然后你在几个
窗口之间来回切切的、再切到一个窗口停一会儿——现象:这一分钟里
没有逐条窗口事件,时间线里过一会冒出一条 presence.digest,正文列应用/窗口累计、切换数、最近状态。
整周期停在同一个窗口也应出现持续时长与 0 次切换。按摘要编号读 view:"timeline",逐段顺序应仍可查看。
再让她 presence_windows——回执里有 [压缩结账] 累计摘要,周期从头开始。
切 {mode:"off"} 后再切几个已经见过的窗口——现象:没有窗口事件;
开一个这个实例没开过的程序——现象:时间线里冒出一条 presence.switched,末尾带「本次运行第一次
见到这个程序」;这时按你的 awayAfterMs 离开一会儿再回来——现象:离开那条(presence.away)
不立刻来(正文会让它说清「最后一次键鼠输入在几点」),离开超过「回来叫醒门槛」(默认 10 分钟)
再回来,回来那条(presence.back)才当场冒出来并报真实离开时长;离开不到门槛就回来,
那条只随她下次开口一起到、不打断她。
改动沿革
2026-10-14(发布日):0.1.0 首发 npm 与公开仓库;随后 0.1.1 把「能判别你离开/回到电脑前」与隐私提示
写进 description 与 README 开头 —— 扩展市场是主要入口,而卡片上只有 description 这一行。
2026-10-14:精简常驻 ENV_PROMPT 和三个工具的描述,合并重复的截图/光标说明;纠正离开不叫醒、 无事件不代表无变化、光标为采样时点等表述。两个切挡回执同步修正离开/回来规则,投递与截图逻辑不变。
同日第二轮:累计摘要、历史分页与跨周期计时——presence.digest 投累计视图,presence_windows
默认结账(当场给、周期重开),view:"timeline" 按记录编号分页读原始时间线。
同日第三轮:离开/回来的时长改从最后一次键鼠输入算起(从前从「发现离开」起算,5 分钟的阈值
会吐出「离开了 30 秒」这种自相矛盾的数),并加「短离开回来不叫醒」的门槛 backWakeAfterMs
(默认 10 分钟)。
