WWW.YOUINFO.SITE
标签聚合 改版

/tag/改版

LinuxDo 最新话题 · 2026-06-11 22:32:18+08:00 · tech

开源推广模板 (点击了解更多详细信息) 我之前开发过一版 开发了一个日语句子分析的网站 能帮助到初学者学习 开发调优 原因是我经常想看句子各个组成部分,老问ai太麻烦了,moji辞书和网上写好的不好用 一怒之下开发了这个玩意 这个玩意基于gemini2.5flash,支持文字识别,直接把文字复制上去就好了 你们只需要自备apikey就好 演示: [5月21日] 体验网站 仓库地址 最近我发现后台不少人还在用(虽然我现在基本不太需要这个东西了)。因为嫌弃之前丑陋的 UI 和 UX,所以我大面积修改和排版了一下。 当然还有个事情就是之前的恶评机器人骂我项目什么刷星到几千k把我炸出来了,然我重新注意到这个东西 这次最大的特点就是加入了 DeepSeek,之前用的是 Gemini,但是额度越来越少而且贵,很多人反馈不够用。后来我发现这个 DeepSeek v4 挺好用的,最重要的是超便宜,感谢梁圣 ,不过可惜无法识别图片。 github.com GitHub - cokice/japanese-analyzer 通过在 GitHub 上创建帐户来为 cokice/japanese-analyzer 开发做出贡献。 1 个帖子 - 1 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-08 16:56:33+08:00 · tech

社区拟优化账号注册机制 GitHub申请渠道 调整为 5 年 GitHub 账号直接获取。 Premium分组 每 3 天可生成 1 个邀请链接。 管理员分组 每 3 天可生成 5 个邀请链接。 今天12点开始 应该都是5年Github的方式注册来的 欢迎各位加入L站 能为社区做出更多贡献 在这里给新来的佬 提个醒 多看 社区准则 真诚 、 友善 、 团结 、 专业 站内很多抽奖贴 但是 非必要不抽奖 一定要切记 不然好不容易来的 被关小黑屋就太可惜了 进一步优化对抽奖帖回复被举报的处理方式 社区通用的是LDC 可以看看 【新人入站从0级到三级经验总结】:升级?赚LDC?LDC有什么用?如何兑换Ai的API Tokens?常用的站内频道?避免被封号注意点?等等,新人可参考,少走弯路! 个人每日使用,站内功能聚合 积分查询 : https://credit.linux.do/ 等级进程 : http://connect.linux.do/ 点数查询 : https://linux.do/leaderboard 社区分数 : 实时数据 - LINUX DO CDK 徽章 : https://linux.do/badges 过盾 : https://linux.do/challenge 改版后来的新佬 改版前的佬 点击以查看投票。 佬们闲来无聊可以来看看 2 个帖子 - 2 位参与者 阅读完整话题

cnBeta全文版 · 2026-06-08 01:05:09+08:00 · tech

据《金融时报》报道,OpenAI 计划在未来数周内推出改版后的 ChatGPT,将其打造成集成编码工具和 AI 代理(agents)的“超级应用”,以强化在企业客户中的竞争力,并在潜在 IPO 前进一步接近盈利目标。 改版后的 ChatGPT 将被设计为一个入口产品,把免费用户引导至更具付费潜力的服务,例如其编码产品 Codex。 报道援引一名 OpenAI 高级员工的话称,“聊天已死”(“Chat is dead”)。 OpenAI 核心产品与平台负责人 Thibault Sottiaux 表示,公司正致力于打造一种产品,让用户拥有一个“能够在生活各个方面为你提供帮助的个人代理,无论是个人事务还是工作”。 这一定位意味着,ChatGPT 不再只是一个对话机器人,而是被重新构想为用户在多场景中调用各类 AI 功能的统一入口。 如果这一思路听起来似曾相识,那是因为自去年以来,业内就不断有关于 OpenAI 押注“超级应用”路径的报道。 今年 3 月,《华尔街日报》曾披露,这一计划标志着 OpenAI 战略方向的重大转变:该公司在 2025 年曾陆续上线多款独立产品,如今则转向通过单一入口来整合体验。 与此同时,OpenAI 管理层公开表示,将逐步放弃诸如视频生成工具 Sora 等被视为“支线任务”(side quests)的项目,以集中资源投入核心产品和平台能力的构建。 在竞争对手 Anthropic 等厂商不断发力企业市场、并筹备上市的背景下,OpenAI 通过“超级应用化”改造 ChatGPT,被视为其调整商业模式、争夺企业级与高价值用户的一次关键尝试。 查看评论

cnBeta全文版 · 2026-06-07 16:35:23+08:00 · tech

自ChatGPT问世掀起人工智能热潮以来,OpenAI即将对其开展 规模最大的一次全面改版 。这家估值8500亿美元的企业计划在今年启动上市流程前,挖掘全新增长动力。公司打算将这款聊天机器人打造为集成编程工具与人工智能智能体的 超级应用 ,推出多款高管眼中创收能力更强的产品。 据十余位现任及前员工透露,此次功能调整是OpenAI整体架构重组的一环。这家总部位于旧金山的企业正调配资源,发力开拓高价值企业客户市场,并与竞争对手Anthropic展开更激烈的角逐。 筹备首次公开募股(IPO)之际,OpenAI面临越来越大的压力,必须提升营收、打通盈利路径。 2022年,在首席执行官萨姆・奥尔特曼的带领下,OpenAI推出ChatGPT,一跃成为生成式AI浪潮的标杆,将人工智能技术带入大众视野。而如今这套新战略,意味着公司发展方向迎来重大转变。 本次改版将大幅提升旗下编程产品Codex的地位,并为其倾斜更多资源。这也体现出公司内部逐渐形成的共识:人工智能的未来,不在于答疑类聊天机器人,而在于能够替用户完成各类事务的智能体。 一名OpenAI资深员工直言:“纯聊天模式已然过时。” ChatGPT上线至今已积累近10亿用户,如今OpenAI管理层愈发将其视作引流入口,借此引导用户使用高附加值产品。目前绝大多数普通用户都在免费使用这款聊天工具。 业内普遍认为,AI智能体可代用户完成订票、整理日程等多项事务,商业价值远超传统聊天机器人。在此背景下,OpenAI正式启动本次改版工作。 与此同时,Codex等产品能够依据用户的简单指令编写代码、开发软件。 本次改版将于 未来数周 逐步落地,初期会先对ChatGPT网页端与移动端界面进行调整,引导用户使用编程、图像生成功能,以及第三方合作应用。 此番调整也让OpenAI的发展策略进一步向Anthropic靠拢。后者深耕企业级产品,实现了飞速发展,而这也将成为OpenAI今年IPO面向投资者推介的核心亮点。 执掌Codex部门、现全面负责OpenAI核心产品与平台业务的蒂博・索蒂奥介绍了本次改版规划:“这次升级不只是表面改动……我们的目标是打造专属个人智能体,无论是生活还是工作场景,它都能全方位为用户提供协助。” 他补充道:“用户可通过手机、电脑或网页端接入使用,驾车途中也能语音和它交互。” 据知情人士透露,Codex的用户大多为付费群体。目前有2万家企业使用OpenAI各类产品,贡献了公司约四成营收,企业相关收入占比预计在今年年底提升至50%。 自今年2月桌面版客户端上线后,Codex周活跃用户数增长至原先的六倍,突破500万。该产品的崛起,也进一步加剧了与Anthropic的竞争,后者旗下的Claude Code现已成为公司增长最快的业务之一。 莱昂尼斯资本合伙人、前OpenAI研究员肖珍妮表示:“大约一年前,OpenAI偏向激进扩张,而Anthropic坚持先实现盈利。如今两家策略逐渐趋同,毕竟双方都筹备上市,相比宏大愿景,投资者更看重实际收益。” 知情人士称,为推动用户使用新服务,OpenAI正在重新设计ChatGPT界面,新增引导入口与功能,推荐编程工具、图像生成服务,以及Canva、Booking.com等合作方应用。 长远来看,OpenAI计划逐步取消这类引导入口,依托自研大模型,让产品能够自动识别用户需求。 今年,OpenAI将ChatGPT、Codex等多个产品团队整合,统一交由索蒂奥管理;包括前产品负责人凯文・韦尔在内的多名高管也已离职。 为全力拓展企业客户,部分面向普通消费者的项目遭到搁置,例如ChatGPT内置购物结算功能。旗下视频生成产品Sora上线未满一年便宣告关停,这也印证了公司的战略重心转变。 管理层认为,未来用户会更倾向使用一体化AI助手,而非各类独立应用。随着智能体能力不断增强,聊天机器人、编程工具、搜索软件等产品的边界也将逐渐模糊。 OpenAI企业产品负责人亚历克斯・恩比里科斯表示:“一旦通用人工智能落地,市面上不会再有大量彼此独立的产品品牌。或许最终只会有一个综合智能体,用户与之对话,就能完成所有需求。” 查看评论

LinuxDo 最新话题 · 2026-06-07 13:14:53+08:00 · tech

OpenAI 计划对 ChatGPT 进行全面翻修,将其重塑为一款"超级应用"。此举旨在 IPO 前夕全力提升公司营收,并迎击来自谷歌及 Anthropic 的激烈竞争。 为了牢牢锁定高价值的企业客户,OpenAI 正将资源激进地向高生产力场景倾斜。该项目计划将核心产品 ChatGPT、编程辅助工具 Codex 以及浏览器产品 Atlas 整合进统一的桌面应用程序中,允许用户无需切换即可在一个界面内完成搜索、写代码与 AI 交互,同时削减多项边缘业务。高管直言"聊天已死",认为能执行任务的代理比聊天更有商业价值。 ​ 为支撑这场野心勃勃的重组并为今年内可能进行的股票上市铺路,OpenAI 正大举扩张团队,预计年底员工规模将从 4500 人暴增至 8000 人,加速向 AI 基础设施商转型。 https://www.ft.com/content/ca0f5f5e-fb9a-41a0-a2a9-0127e15b7db9 7 个帖子 - 7 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-06 19:23:04+08:00 · tech

原帖方法博主博文: Giffgaff eSIM直装版:直接获取eSIM激活码 - Simon (Yu Ma) :这个方法是最新方法,不是现在主流的 hook 沙盒环境方式。流程已经简化很多了。 照着教程走了一遍,全程魔改版 gg app里面操作。 十分钟不到顺利下卡。上面的博文还是很详细的。 觉得魔改版app不放心,也可以在安卓模拟器里安装了 做了个视频教程,有需要朋友可以围观: 海外保号神卡 giffgaff 手机号|别人教 7 步,我只要 3 步搞定|最新境外手机号申请教程 eSIM 4 个帖子 - 3 位参与者 阅读完整话题

IT之家 · 2026-06-06 09:17:03+08:00 · tech

IT之家 6 月 6 日消息,彭博社的马克 · 古尔曼(Mark Gurman)今天(6 月 6 日)发布博文,详细梳理了苹果 iOS 27 系统带来的诸多新特性 / 新功能。 IT之家援引博文介绍,本次 iOS 27 系统升级主要聚焦 Siri 和 Apple Intelligence 技能拓展上,不过苹果同步调整一些用户使用频率很高的系统应用,延伸 AI 能力至界面、交互和图片处理体验。 查找(Find My)应用方面,苹果将会在 iOS 27 中升级视觉体验,会更新底部导航标签栏图标在内的诸多元素。 照片(Photos)应用将增强 Clean Up 功能,继续强化照片整理与修饰能力,让用户减少对第三方应用的依赖,在系统原生应用内完成更多图片处理。 古尔曼还透露苹果正在测试基于自然语言提示的照片编辑功能。 用户未来可能通过语音或文字,直接告诉系统“裁剪图片”或“调整颜色” ,照片应用再按指令完成编辑。这项功能可能不会在 iOS 27 的首个版本中推出。 在系统交互层面,iOS 27 会为来电通知添加新的动画效果。此外用户从左侧下滑将打开通知中心,而从屏幕中部下滑,则会打开搜索或 Ask AI 面板。

LinuxDo 最新话题 · 2026-06-05 11:30:24+08:00 · tech

可能有些朋友还记得我去年在论坛里开源了一款 Deep Research,在大家的支持下,功能越来越完善,很快就达到了当时人力开发的功能极限。那时候的 Deep Research 已经非常完善了,作为当时依然 古法编程 的我而言,已经开始无力再深入开发了。 我后来开始借助 Claude Code 对项目从零开始重构(没有在之前项目的分支上开发,使用了新的 GitHub 仓库),借助 Any 大善人的 API,断断续续地开发了一段时间,那时候的研究报告生成已经超越了之前的那个版本,不过后来 Any 大善人变得不稳定,所以期间又搁置了很久。上个月成功订阅了 Claude Code,借助 Claude Code 我让 AI 进行了几次大版本的更新。最近,我完成了其他项目的开发,终于腾出手来完整调试这个项目,昨天看到最终产出的研究报告时,差点惊坐在地,这…也太牛了吧! 沐曦集成电路(Metax)全面尽职调查研究.txt (107.3 KB) 直接生成了 12 万字的深度研究报告…这还不是终点,只是我随手生成的一个测试用例罢了…目前我不确定是 AI 变得强大了,还是我的项目底层架构写得好…我后续换一个 AI 模型再试试… 目前项目依然还在开发调试过程中…SAAS 架构的版本不会完整开源,会考虑给 L 站的佬友提供订阅优惠(争取支持 LDC 订阅)。当然,我会考虑开源一个 CE 版本(单用户的版本)。由于这几个月论坛政治变化较大,项目无法像之前那样进行发布,因为开源推广要求全部开源,我不确定是否会因为存在 SAAS 版本和 CE 版本的区别而被恶意举报…我可是快要飞升 L3 的呀 。 10 个帖子 - 10 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-06-03 12:29:48+08:00 · tech

开启自定义翻译专家,我这里面用的是GLM5.1,但是任何模型应该都差不多 本方法的目的是用于进行中文辅助的英文文档阅读,在不流失语境和学习能力的情况下进行信息吸收和阅读,我英文大概是B2水平,不上不下,看东西磕磕绊绊,纯看翻译版本吧还觉得不能总这样 原翻译文本(谷歌) 后翻译(AI提示词优化的) 实践方法如下 设置系统提示词汇为 You are a professional English reading assistant for a Chinese B2-level English learner. Your task is NOT to translate the whole English text into Chinese. Your task is to keep the original English text and add selective Chinese learning annotations after difficult English words or phrases, so the reader can read English quickly while learning vocabulary naturally. ## Core Output Style Output the original English text almost exactly as written. When you encounter an English word, phrase, idiom, phrasal verb, academic expression, abstract noun, or important collocation that may be difficult for a B2-level Chinese learner, add a short Chinese annotation immediately after it in parentheses. Example: Prefer making progress over stopping for clarification(澄清) when the request is already clear enough to attempt. Use context(上下文) and reasonable assumptions(合理假设) to move forward. ## Annotation Principles 1. Keep the English sentence as the main text. 2. Add Chinese annotations only for words or phrases that are likely to slow down a B2-level learner. 3. Do NOT annotate every word. 4. Do NOT annotate very common words unless their usage is abstract, idiomatic, or important in context. 5. Prefer annotating meaningful chunks rather than isolated words when a phrase is more useful. * Good: make progress(取得进展) * Good: reasonable assumptions(合理假设) * Bad: reasonable(合理的) assumptions(假设) when the phrase is better learned together 6. The Chinese annotation should explain the meaning in this context, not every dictionary meaning. 7. Keep annotations short, natural, and educational. 8. Usually use 2–8 Chinese characters for a single word, and 2–12 Chinese characters for a phrase. 9. For abstract or academic words, make the annotation slightly explanatory if needed. * materially(实质性地) * implication(隐含意义) * trade-off(权衡) 10. For proper nouns, code, variables, formulas, URLs, product names, and technical identifiers, keep the original text and do not annotate unless the term itself is important for understanding. ## Difficulty Target Assume the reader is around CEFR B2 / IELTS 6.0–6.5 / CET-6 upper-intermediate level. Prioritize annotations for: * IELTS / CET-6 medium-high difficulty vocabulary * CEFR B2–C1 vocabulary * academic verbs and nouns * abstract concepts * logical connectors * idioms and phrasal verbs * fixed collocations * words whose meaning changes significantly in context * phrases important for understanding the author’s argument Avoid annotating: * basic A1–B1 words * repeated words that have already been annotated recently * obvious function words * simple grammar words * names, unless necessary ## Annotation Density Balance reading speed and learning. For normal text, annotate about 3–8 items per 100 English words. If the text is very easy, annotate fewer. If the text is dense, academic, legal, technical, or philosophical, annotate more, but still avoid clutter. When the same word or phrase appears repeatedly: * Annotate it the first time. * Do not annotate every later occurrence unless the meaning changes. ## Formatting Rules 1. Output only the processed English text with Chinese annotations. 2. Do not add explanations, summaries, titles, comments, or extra notes. 3. Keep exactly the same number of paragraphs and the same paragraph structure as the original. 4. Preserve the original formatting as much as possible. 5. If the text contains HTML tags, preserve the HTML structure and place annotations naturally inside the readable text. 6. For content that should not be changed, such as code, commands, variables, file paths, formulas, URLs, proper nouns, and placeholders, keep it unchanged. 7. If input contains %%, use %% in your output as paragraph separators. 8. If input has no %%, do not add %%. 9. Do not translate complete sentences into Chinese. 10. Do not put annotations before the English word. Always put them immediately after the English word or phrase. ## OUTPUT FORMAT * Single paragraph input → Output the processed English paragraph directly. * Multi-paragraph input → Use %% as the paragraph separator between processed paragraphs. ## Examples ### Input: Prefer making progress over stopping for clarification when the request is already clear enough to attempt. Use context and reasonable assumptions to move forward. Ask for clarification only when the missing information would materially change the answer or create meaningful risk, and keep any question narrow. ### Output: Prefer making progress(取得进展) over stopping for clarification(澄清) when the request is already clear enough to attempt(尝试处理). Use context(上下文) and reasonable assumptions(合理假设) to move forward. Ask for clarification only when the missing information would materially(实质性地) change the answer or create meaningful risk(重大风险), and keep any question narrow(范围狭窄). ### Input: The proposal highlights the trade-off between efficiency and interpretability in large-scale decision systems. ### Output: The proposal(提案) highlights(强调) the trade-off(权衡) between efficiency(效率) and interpretability(可解释性) in large-scale(大规模的) decision systems(决策系统). {{title_prompt}}{{summary_prompt}}{{terms_prompt}} {{imt_style_guide}} 多段提示词: Annotate English for a B2 Chinese learner. Keep the original English text. Do not translate full sentences. Add short Chinese notes in parentheses after difficult B2–C1 words, phrases, idioms, or collocations. Annotate selectively, about 3–8 items per 100 words. Prefer useful phrases over single words. Do not annotate basic words, names, code, URLs, or repeated terms. Preserve the original format, paragraphs, HTML tags, and %% separators. Text: {{text}} 单段提示词 Keep the original English text. Add short Chinese notes in parentheses after difficult B2–C1 words, idioms, or useful phrases. Annotate selectively. Do not translate full sentences. Preserve the original format. Output only the annotated text. {{text}} 欢迎各位佬友讨论 2 个帖子 - 2 位参与者 阅读完整话题

IT之家 · 2026-06-02 06:54:02+08:00 · tech

IT之家 6 月 2 日消息,苹果 2026 年度全球开发者大会(WWDC)将于下周启幕,苹果官方以全新宣传语“全高光就位(All systems glow)”为这场活动预热造势。 据IT之家了解,“All systems glow”改编自英文惯用短语“all systems go(一切就绪)”,大概率暗喻传闻中将登陆 iOS 27 的全新 Siri 界面设计。据彭博社制作的示意图显示,Siri 将推出独立 App,同时 iPhone 灵动岛新增的“搜索或提问”功能均采用深色 UI 搭配发光视觉元素。 苹果此前为 2026 开发者大会拟定的标语为“Coming bright up”,该标语与活动配套视觉素材同样指向 Siri 的全新改版。 本届 WWDC 2026 首场主题发布会定于太平洋时间 6 月 8 日上午 10 点召开(北京时间 6 月 9 日凌晨 1 点)。苹果届时将正式发布 iOS 27、iPadOS 27、macOS 27、watchOS 27、tvOS 27 以及 visionOS 27 全系列系统;大会持续至 6 月 12 日(周五),数百场开发者技术讲座将全程线上放送。 本次主题演讲可在苹果官网、Apple TV 应用以及 YouTube 平台收看直播。

IT之家 · 2026-06-01 20:25:10+08:00 · tech

从 8.0.72 直接到 8.0.74,在落后两个大版本的情况下,微信安卓版通过一次跳远,实现了版本号追齐 iOS。 在 5 月 29 日晚间, 微信安卓平台迎来 8.0.74 测试版 ,然而几小时后便撤包,直到昨晚(5 月 31 日)又重新发布链接,恢复正常下载。 经实测发现,本次微信安卓最新内测的重心不在于上线新花样,而是以灰度功能覆盖为主。 具体的更新情况,IT之家小编接下来就逐一向各位说明。 一、分享为贴图 和微信 iOS 端 8.0.74 正式版一致的新变化,照片“分享为贴图”同样在安卓端上线。 在手机相册 App 中选中任意一张图片后,点击分享至微信, 弹出的选项中新增“分享为贴图” 。 点击该选项后,系统会自动拉起微信,并跳转至贴图的编辑页面,所选图片会直接被添加至编辑器中。 如此一来,发布贴图的步骤能得到简化,可以将“上传图片”的操作做在前面,无需再在编辑器中翻找图片。 二、公众号留言显示首评 每次我们IT之家微信公众号发布推文后,总是有朋友喜欢在留言区中“抢沙发”。 以前抢到沙发,实则没啥特别的,别人根本看不出来,但是现在不一样了。 若是第一个给文章留言且被小编精选出来, 那么留言下方会出现绿色字体的“首评” ,拥有了独特的标识。 三、转账支持组合支付 不光是 iOS 端,伴随着安卓端新版本来袭,“转账支持组合支付”功能也从灰度测试转为全量覆盖。 给微信好友转账时,点击付款方式右侧的“更改”,右上角新增“组合支付”, 可同时勾选零钱、零钱通、多张银行卡等最多两个付款方式 ,付款金额可自由填写,系统会自动计算补足差额。 得益于此,就不怕转账时单一付款方式余额不够了,不必再去凑齐或分转,组合支付就能搞定。 四、灰度功能盘点 鉴于微信新功能上线用的是灰度分批上线的方式,因此此次安卓版 8.0.74 测试版与 iOS 版 8.0.74 正式版的情况一致, 尚且有多个功能处于灰度测试中 。 IT之家小编汇总如下: 多图发送后合并展示 给好友发送图片和视频,若选择三张及其以上的内容,会弹出“发送后合并展示”选项。 勾选该选项后再点击发送,发出的图片和视频会以堆叠的样式展示,支持展开和收起。 全新朋友圈样式 在微信 iOS 版 8.0.71 版本中,微信对于朋友圈的页面排版进行灰度改版。 在“我的朋友圈”页面内,原本位于配图右侧的文字描述调整至配图上方,发布日期的显示形式也同步进行了精简。 后续,这一全新朋友圈样式也在安卓版启动灰测,但时至今日都没有全量覆盖。 朋友圈相册优化 和朋友圈改版相辅相成的优化,朋友圈相册的调整同样在安卓版展开灰测。 “我的朋友圈”页面右上角新增“朋友圈相册”入口,支持按年 / 月 / 日缩放浏览历史动态,逻辑类似苹果原生相册。 听一听新增“AI 翻唱模型” 依次点开“发现-听一听-个人中心-设置”,页面内新增“AI 翻唱模型”入口。 有两个模型可供选择,分别为:听一听音乐模型,腾讯音乐阿波罗。 聊天消息可多选复制 在聊天界面“多选”多条消息后,底部选项栏中新增“复制”按钮,可以一键复制多条消息并发送。 发送前可通过“详情”查看复制的消息,发送后每条消息是独立的,带有发送者的昵称和发送时间。 五、总结 关于微信安卓版 8.0.74 测试版的更新情况,IT之家小编就说明到这里。 与 iOS 相比,缺少了“本机号码登录”等功能的灰度,希望后续能加紧开发并尽早上线。 也期待在 8.0.74 正式版到来之时, 微信能为广大安卓用户安排更多实用功能 。 大家在 IT之家微信号 回复“ 微信 ”两字,即可获取当前最新官方内部版微信下载。

cnBeta全文版 · 2026-05-29 14:06:33+08:00 · tech

微软近日开始向 Microsoft 365 用户推送新一轮 Copilot 改版更新,该公司称此次更新源自其正在推进的“Copilot 设计体系”计划,由新任首席设计官 Jon Friedman 牵头,目标是让 Copilot 成为“随时可用、不过分打扰”的思考与协作伙伴,从而切实提升生产力体验。 在这一思路下,首批成果已经出现在 Microsoft 365 Copilot 独立应用中,并将逐步扩展到各类 Office 应用场景。 在界面层面,Copilot 应用中的提示输入框(prompt box)被明显放大,并在输入区域下方加入一排紧贴任务场景的工具和控制项,方便用户在同一界面完成更多操作。 微软认为,这种更简洁、操作路径更短的界面设计可以带来更快、更灵敏的交互体验,尤其是相较于既反应迟缓又输出不连贯的传统界面,新的设计更能跟上用户思路和工作节奏。 本次改版的一个重要理念是通过引入 Work IQ 智能层来落实“渐进揭示”(progressive disclosure)原则:界面左侧新增可展开的侧边面板,用于承载附加功能和内容;主输入框则支持内联格式化等操作,在不堆砌控件的前提下按需展示更多能力。 微软强调,系统会根据用户当前任务的复杂度和上下文,动态浮现更多功能和集成,而不是一开始就铺陈所有选项,从而降低认知负担,让界面更易于理解和导航。 在智能与性能方面,Copilot 的回答将更广泛地基于 Work IQ 提供的上下文信息进行“落地”,同时用户还能在应用中选择不同的 AI 模型,以适配不同场景需求。 微软还宣称,最新版本的 Copilot 应用整体运行速度提升到此前的两倍,对复杂问题的响应时间也缩短了约 10%,旨在保证在高负载、复杂请求场景下依然保持较高可用性和流畅度。 在使用情况方面,微软对 Copilot 在 Office 各应用中的采纳度给出了最新数据:在引入新 Copilot 体验后,Word 中的 Copilot 使用量上升了 27%,Excel 上升 33%,PowerPoint 上升 43%,Outlook 上升 30%。 不过,微软也在“细则”中说明,这些数字基于功能推出后短短几天内的样本数据,只能反映短期刺激效果,并不足以代表长期趋势。 功能扩展上,Copilot 目前提供了更多“代理式”(agentic)体验和新的调用入口,但并非所有尝试都获得用户好评,例如 Excel 中无法移除的 Copilot 按钮就曾引发用户强烈反弹。 对此,微软已计划对部分不受欢迎的入口进行回退或调整,以在“主动融入工作流”和“不过度打扰用户”之间寻找平衡。 微软表示,此次 Copilot 改版标志着产品重点从单一功能堆叠转向“互联体验”,即通过整体设计将各类 AI 能力串联起来,而非分别孤立存在。 这也意味着最新的改动不仅仅是界面更快、更“好看”,更是试图让 Copilot 的交互方式更具人性化,通过更自然的呈现方式和上下文感知,让用户把它视作工作环境的一部分而不是额外负担。 对于有兴趣了解更多具体设计细节和产品规划的用户,微软已在其官方 Microsoft 365 博客上发布了专门文章,对此次 Copilot 新设计进行进一步阐述和展示: https://www.microsoft.com/en-us/microsoft-365/blog/2026/05/28/introducing-a-new-design-for-microsoft-365-copilot/ 查看评论

LinuxDo 最新话题 · 2026-05-25 19:24:36+08:00 · tech

Sub2API 账号模型巡检油猴脚本改版分享 说明:这个脚本是在参考站内一位大佬的油猴脚本基础上改出来的,主要是按我自己的使用场景做了一些调整。原思路来自前辈,感谢站内大佬的分享;如果有不合适的地方可以提醒我修改或删除。 主要修改 这版主要改了两个地方: 支持按分组收集账号并测活 脚本会尽量识别当前账号列表页面的分组。 拉取账号时只处理当前分组下的账号。 如果没识别到分组,会弹窗让你手动填写本次要测活的分组。 增加账号并发数设置 面板里可以直接设置账号级并发数。 默认并发数是 8 。 最大限制为 50 ,个人建议先从 5-15 之间试。 脚本功能 在后台账号列表页右侧注入一个“账号巡检”按钮。 自动尝试捕获当前页面请求里的 Authorization 。 如果自动捕获失败,也可以在面板里手动填写 Bearer token 。 按当前分组拉取账号列表。 对可调度账号进行模型测活。 已经关闭调度的账号会跳过。 任一测试模型异常时,会自动关闭该账号的 schedulable 。 可以设置: 单模型超时时间 账号并发数 测试模型 使用方式 安装 Tampermonkey / 油猴扩展。 新建脚本,把脚本内容粘进去保存。 打开后台账号列表页: https://{改成你自己的域名}/admin/accounts 页面右侧会出现“账号巡检”按钮。 展开面板后确认: Authorization 是否已自动捕获 当前分组是否识别正确 并发数是否合适 测试模型是否正确 点击“开始巡检”。 注意事项 这个脚本会在模型异常时自动关闭账号调度,建议先小范围、小并发测试。 并发数不要一上来拉太高,避免请求过多导致误判或给服务造成压力。 如果页面没有自动捕获到 Authorization ,可以刷新页面后再试,或者手动填写。 如果当前分组没有识别成功,开始时会提示手动填写。 脚本仅用于自己后台账号巡检场景,请确认自己有相关管理权限后再使用。 下面是正文里可以补一句的话: 完整脚本 // ==UserScript== // @name Sub2API 账号模型巡检并自动下线 // @namespace https://sinry.example // @version 0.1.1 // @description 按当前页面分组批量测试账号模型;任一模型异常时自动关闭账号 schedulable;已关闭调度账号跳过 // @match https://{改成你自己的域名}/admin/accounts* // @run-at document-start // @grant none // ==/UserScript== (function () { 'use strict'; const CONFIG = { apiBase: location.origin, pageSize: 100, defaultTimeoutMs: 45000, defaultConcurrency: 8, maxConcurrency: 50, prompt: 'hi', onlyCheckSchedulable: false, stopOnFirstModelFailure: true, preferredModels: ['gpt-5.4', 'gpt-4o-mini', 'gpt-4o', 'gpt-4.1', 'gpt-4.1-mini'], defaultTestModel: 'gpt-5.4', groupParamNames: ['group', 'groups', 'account_group', 'accountGroup'], pageAuthTokenKey: 'auth_token', authStorageKey: '__sub2api_checker_auth__', timeoutStorageKey: '__sub2api_checker_timeout_ms__', concurrencyStorageKey: '__sub2api_checker_concurrency__', testModelStorageKey: '__sub2api_checker_test_model__', currentGroupStorageKey: '__sub2api_checker_current_group__', autoDisableStorageKey: '__sub2api_checker_auto_disable__', }; function getCachedAuthToken() { const raw = localStorage.getItem(CONFIG.pageAuthTokenKey) || sessionStorage.getItem(CONFIG.pageAuthTokenKey) || localStorage.getItem(CONFIG.authStorageKey) || ''; return raw ? (raw.startsWith('Bearer ') ? raw : `Bearer ${raw}`) : ''; } function clampConcurrency(value) { const n = Math.floor(Number(value)); if (!Number.isFinite(n)) return CONFIG.defaultConcurrency; return Math.min(CONFIG.maxConcurrency, Math.max(1, n)); } function readSavedConcurrency() { return clampConcurrency(localStorage.getItem(CONFIG.concurrencyStorageKey) || CONFIG.defaultConcurrency); } function readSavedAutoDisable() { return localStorage.getItem(CONFIG.autoDisableStorageKey) !== 'false'; } const state = { authHeader: getCachedAuthToken(), timeoutMs: Number(localStorage.getItem(CONFIG.timeoutStorageKey) || CONFIG.defaultTimeoutMs), concurrency: readSavedConcurrency(), testModel: localStorage.getItem(CONFIG.testModelStorageKey) || CONFIG.defaultTestModel, autoDisable: readSavedAutoDisable(), currentGroup: '', running: false, stopRequested: false, panelReady: false, collapsed: true, stats: { total: 0, checked: 0, active: 0, started: 0, ok: 0, enabled: 0, disabled: 0, skipped: 0, failed: 0, }, }; function log(msg, type = 'info') { const time = new Date().toLocaleTimeString(); const line = `[${time}] ${msg}`; console[type === 'error' ? 'error' : 'log'](`[sub2api-checker] ${line}`); const box = document.querySelector('#sub2api-checker-log'); if (!box) return; const color = type === 'error' ? '#ff7875' : type === 'warn' ? '#ffd666' : type === 'success' ? '#95de64' : '#d9d9d9'; const row = document.createElement('div'); row.style.color = color; row.textContent = line; box.appendChild(row); box.scrollTop = box.scrollHeight; } function saveAuth(auth) { if (!auth || typeof auth !== 'string') return; const normalized = auth.startsWith('Bearer ') ? auth : `Bearer ${auth}`; const changed = state.authHeader !== normalized; state.authHeader = normalized; localStorage.setItem(CONFIG.authStorageKey, normalized); const input = document.querySelector('#sub2api-checker-auth'); if (input && !input.value) input.value = normalized; if (changed) log('已捕获 Authorization', 'success'); } function saveTimeoutMs(timeoutMs) { const n = Number(timeoutMs); if (!Number.isFinite(n) || n < 1000) return false; state.timeoutMs = n; localStorage.setItem(CONFIG.timeoutStorageKey, String(n)); const input = document.querySelector('#sub2api-checker-timeout'); if (input) input.value = String(Math.floor(n / 1000)); return true; } function saveConcurrency(concurrency) { const n = clampConcurrency(concurrency); state.concurrency = n; localStorage.setItem(CONFIG.concurrencyStorageKey, String(n)); const input = document.querySelector('#sub2api-checker-concurrency'); if (input) input.value = String(n); return true; } function saveTestModel(model) { const normalized = String(model || '').trim(); if (!normalized) return false; state.testModel = normalized; localStorage.setItem(CONFIG.testModelStorageKey, normalized); const input = document.querySelector('#sub2api-checker-test-model'); if (input) input.value = normalized; return true; } function saveAutoDisable(enabled) { state.autoDisable = !!enabled; localStorage.setItem(CONFIG.autoDisableStorageKey, String(state.autoDisable)); const input = document.querySelector('#sub2api-checker-auto-disable'); if (input) input.checked = state.autoDisable; return true; } function normalizeGroup(value) { if (value === undefined || value === null) return ''; const text = String(value).trim(); if (!text) return ''; if (['全部', '全部分组', 'all', 'null', 'undefined'].includes(text.toLowerCase())) return ''; return text; } function getGroupFromUrl(urlLike = location.href) { try { const url = new URL(String(urlLike), location.origin); for (const name of CONFIG.groupParamNames) { const value = normalizeGroup(url.searchParams.get(name)); if (value) return value; } } catch (_) {} return ''; } function getElementMeta(el) { const parts = [ el.getAttribute('name'), el.id, el.getAttribute('aria-label'), el.getAttribute('title'), el.getAttribute('placeholder'), typeof el.className === 'string' ? el.className : '', ]; if (el.labels) { parts.push(...Array.from(el.labels).map((label) => label.textContent || '')); } const closestLabel = el.closest('label')?.textContent || ''; if (closestLabel) parts.push(closestLabel); const parentText = el.parentElement?.textContent || ''; if (parentText.length < 120) parts.push(parentText); return parts.filter(Boolean).join(' ').toLowerCase(); } function looksLikeGroupControl(el) { const meta = getElementMeta(el); return meta.includes('group') || meta.includes('分组'); } function getGroupFromDom() { const controls = Array.from(document.querySelectorAll('select,input')).filter(looksLikeGroupControl); for (const el of controls) { const value = normalizeGroup( el.tagName === 'SELECT' ? el.value || el.selectedOptions?.[0]?.value || el.selectedOptions?.[0]?.textContent || '' : el.value || el.getAttribute('value') || '' ); if (value) return value; } const labelEls = Array.from(document.querySelectorAll('label,.ant-form-item-label,[class*="label"],[class*="Label"]')) .filter((el) => /分组|group/i.test(el.textContent || '')); for (const label of labelEls) { const container = label.closest('.ant-form-item') || label.parentElement; const candidates = [ container?.querySelector('select')?.value, container?.querySelector('select')?.selectedOptions?.[0]?.value, container?.querySelector('select')?.selectedOptions?.[0]?.textContent, container?.querySelector('input')?.value, container?.querySelector('.ant-select-selection-item')?.getAttribute('title'), container?.querySelector('.ant-select-selection-item')?.textContent, container?.querySelector('[class*="singleValue"]')?.textContent, container?.querySelector('[class*="selected"]')?.textContent, ]; for (const candidate of candidates) { const value = normalizeGroup(candidate); if (value) return value; } } return ''; } function readCurrentGroup() { return getGroupFromUrl(location.href) || getGroupFromDom(); } function updateGroupDisplay() { const el = document.querySelector('#sub2api-checker-current-group'); if (!el) return; el.textContent = state.currentGroup || '未识别'; el.style.color = state.currentGroup ? '#95de64' : '#ffd666'; } function saveCurrentGroup(group, source = '') { const normalized = normalizeGroup(group); if (!normalized) return false; const changed = state.currentGroup !== normalized; state.currentGroup = normalized; localStorage.setItem(CONFIG.currentGroupStorageKey, normalized); updateGroupDisplay(); if (source && changed) { log(`已识别当前分组:${normalized}(${source})`, 'success'); } return true; } async function injectAuthSniffer() { while (!document.documentElement) { await new Promise((resolve) => setTimeout(resolve, 0)); } const script = document.createElement('script'); const nonce = document.querySelector('script[nonce]')?.nonce || document.querySelector('script[nonce]')?.getAttribute('nonce'); if (nonce) script.nonce = nonce; script.textContent = ` (() => { const groupParamNames = ${JSON.stringify(CONFIG.groupParamNames)}; const emit = (auth) => { if (!auth) return; document.dispatchEvent(new CustomEvent('__sub2api_checker_auth__', { detail: auth })); }; const emitGroup = (urlLike) => { try { if (!urlLike) return; const url = new URL(String(urlLike), location.origin); if (!url.pathname.includes('/api/v1/admin/accounts')) return; for (const name of groupParamNames) { const value = (url.searchParams.get(name) || '').trim(); if (value) { document.dispatchEvent(new CustomEvent('__sub2api_checker_group__', { detail: value })); return; } } } catch (_) {} }; const pickAuth = (headersLike) => { try { if (!headersLike) return ''; if (headersLike instanceof Headers) { return headersLike.get('Authorization') || headersLike.get('authorization') || ''; } if (Array.isArray(headersLike)) { for (const [k, v] of headersLike) { if (String(k).toLowerCase() === 'authorization') return v || ''; } return ''; } if (typeof headersLike === 'object') { for (const key of Object.keys(headersLike)) { if (key.toLowerCase() === 'authorization') return headersLike[key] || ''; } } } catch (_) {} return ''; }; const origFetch = window.fetch; if (origFetch) { window.fetch = function(input, init) { const auth = pickAuth(init && init.headers) || pickAuth(input && input.headers); if (auth) emit(auth); const requestUrl = typeof input === 'string' || input instanceof URL ? String(input) : input && input.url; emitGroup(requestUrl); return origFetch.apply(this, arguments); }; } const origOpen = XMLHttpRequest.prototype.open; const origSetHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.open = function() { this.__sub2apiAuth = ''; emitGroup(arguments[1]); return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function(name, value) { if (String(name).toLowerCase() === 'authorization' && value) { this.__sub2apiAuth = value; emit(value); } return origSetHeader.apply(this, arguments); }; })(); `; document.documentElement.appendChild(script); script.remove(); document.addEventListener('__sub2api_checker_auth__', (event) => { saveAuth(event.detail); }); document.addEventListener('__sub2api_checker_group__', (event) => { saveCurrentGroup(event.detail, '账号列表请求'); }); } function updateStats() { const el = document.querySelector('#sub2api-checker-stats'); if (!el) return; const s = state.stats; const queued = Math.max(0, s.total - s.checked - s.active); el.textContent = `总数 ${s.total} | 运行中 ${s.active} | 队列 ${queued} | 已处理 ${s.checked} | 正常 ${s.ok} | 已启用 ${s.enabled} | 已关闭 ${s.disabled} | 跳过 ${s.skipped} | 异常 ${s.failed}`; } function updatePanelCollapsed() { const shell = document.querySelector('#sub2api-checker-shell'); const root = document.querySelector('#sub2api-checker-panel'); const toggle = document.querySelector('#sub2api-checker-toggle'); if (!root || !toggle || !shell) return; root.style.width = state.collapsed ? '0px' : '420px'; root.style.opacity = state.collapsed ? '0' : '1'; root.style.marginRight = state.collapsed ? '0px' : '12px'; root.style.pointerEvents = state.collapsed ? 'none' : 'auto'; root.style.transform = state.collapsed ? 'translateX(12px)' : 'translateX(0)'; toggle.textContent = state.collapsed ? '账号巡检' : '收起'; toggle.style.borderRadius = state.collapsed ? '10px 0 0 10px' : '10px'; shell.style.pointerEvents = 'auto'; } function ensurePanel() { if (state.panelReady) return; state.panelReady = true; const shell = document.createElement('div'); shell.id = 'sub2api-checker-shell'; shell.style.cssText = ` position: fixed; right: 0; top: 120px; z-index: 1000000; display: flex; flex-direction: row; align-items: flex-start; pointer-events: auto; `; document.body.appendChild(shell); const toggle = document.createElement('button'); toggle.id = 'sub2api-checker-toggle'; toggle.style.cssText = ` padding: 10px 8px; border: 0; border-radius: 10px 0 0 10px; background: #1677ff; color: #fff; cursor: pointer; writing-mode: vertical-rl; text-orientation: mixed; box-shadow: 0 8px 24px rgba(0,0,0,.25); transition: transform .28s ease, box-shadow .28s ease, border-radius .28s ease; font: 12px/1.2 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,PingFang SC,Microsoft YaHei,sans-serif; `; toggle.addEventListener('mouseenter', () => { toggle.style.transform = 'translateX(-2px)'; toggle.style.boxShadow = '0 10px 28px rgba(0,0,0,.32)'; }); toggle.addEventListener('mouseleave', () => { toggle.style.transform = 'translateX(0)'; toggle.style.boxShadow = '0 8px 24px rgba(0,0,0,.25)'; }); toggle.addEventListener('click', () => { state.collapsed = !state.collapsed; updatePanelCollapsed(); }); shell.appendChild(toggle); const root = document.createElement('div'); root.id = 'sub2api-checker-panel'; root.style.cssText = ` width: 0; opacity: 0; overflow: hidden; transition: width .28s ease, opacity .22s ease, margin-right .28s ease, transform .28s ease; transform: translateX(12px); `; root.innerHTML = ` <div id="sub2api-checker-panel-inner" style=" width:420px; background:rgba(16, 18, 27, 0.96); color:#fff; border:1px solid #30363d; border-radius:12px; box-shadow:0 8px 24px rgba(0,0,0,.35); font:12px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,PingFang SC,Microsoft YaHei,sans-serif; overflow:hidden; "> <div style="padding:12px 14px;border-bottom:1px solid #30363d;font-weight:700;">Sub2API 账号模型巡检</div> <div style="padding:12px 14px;display:flex;flex-direction:column;gap:8px;"> <label style="display:flex;flex-direction:column;gap:4px;"> <span>Authorization(优先自动捕获,抓不到再手填)</span> <input id="sub2api-checker-auth" type="text" placeholder="Bearer xxxxxx" style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #434a57;background:#111723;color:#fff;" /> </label> <label style="display:flex;flex-direction:column;gap:4px;"> <span>单模型超时时间(秒)</span> <input id="sub2api-checker-timeout" type="number" min="1" step="1" placeholder="45" style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #434a57;background:#111723;color:#fff;" /> </label> <label style="display:flex;flex-direction:column;gap:4px;"> <span>账号并发数(1-${CONFIG.maxConcurrency},建议 5-15)</span> <input id="sub2api-checker-concurrency" type="number" min="1" max="${CONFIG.maxConcurrency}" step="1" placeholder="8" style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #434a57;background:#111723;color:#fff;" /> </label> <label style="display:flex;flex-direction:column;gap:4px;"> <span>测试模型</span> <input id="sub2api-checker-test-model" type="text" placeholder="gpt-5.4" style="width:100%;box-sizing:border-box;padding:8px;border-radius:8px;border:1px solid #434a57;background:#111723;color:#fff;" /> </label> <label style="display:flex;gap:8px;align-items:center;color:#d9d9d9;"> <input id="sub2api-checker-auto-disable" type="checkbox" style="margin:0;" /> <span>模型异常时自动关闭账号调度</span> </label> <div style="display:flex;gap:8px;align-items:center;color:#bfbfbf;"> <span>当前页面分组:</span> <strong id="sub2api-checker-current-group" style="flex:1;color:#ffd666;">未识别</strong> <button id="sub2api-checker-refresh-group" style="padding:6px 8px;border:0;border-radius:8px;background:#434a57;color:#fff;cursor:pointer;">重新读取</button> </div> <div style="display:flex;gap:8px;align-items:center;"> <button id="sub2api-checker-start" style="flex:1;padding:8px 10px;border:0;border-radius:8px;background:#1677ff;color:#fff;cursor:pointer;">开始巡检</button> <button id="sub2api-checker-stop" style="flex:1;padding:8px 10px;border:0;border-radius:8px;background:#fa541c;color:#fff;cursor:pointer;">停止</button> </div> <div id="sub2api-checker-stats" style="color:#bfbfbf;">总数 0 | 运行中 0 | 队列 0 | 已处理 0 | 正常 0 | 已启用 0 | 已关闭 0 | 跳过 0 | 异常 0</div> <div id="sub2api-checker-log" style="height:320px;overflow:auto;background:#0b0f17;border:1px solid #30363d;border-radius:8px;padding:8px;"></div> </div> </div> `; shell.appendChild(root); const authInput = root.querySelector('#sub2api-checker-auth'); authInput.value = state.authHeader; authInput.addEventListener('change', () => { const v = authInput.value.trim(); if (v) saveAuth(v); }); const timeoutInput = root.querySelector('#sub2api-checker-timeout'); timeoutInput.value = String(Math.floor(state.timeoutMs / 1000)); timeoutInput.addEventListener('change', () => { const sec = Number(timeoutInput.value || 0); if (!saveTimeoutMs(sec * 1000)) { timeoutInput.value = String(Math.floor(state.timeoutMs / 1000)); log('超时时间无效,需大于等于 1 秒', 'error'); return; } log(`已设置单模型超时 ${sec} 秒`, 'success'); }); const concurrencyInput = root.querySelector('#sub2api-checker-concurrency'); concurrencyInput.value = String(state.concurrency); concurrencyInput.addEventListener('change', () => { saveConcurrency(concurrencyInput.value || CONFIG.defaultConcurrency); log(`已设置账号并发数 ${state.concurrency}`, 'success'); }); const testModelInput = root.querySelector('#sub2api-checker-test-model'); testModelInput.value = state.testModel; testModelInput.addEventListener('change', () => { const model = testModelInput.value.trim(); if (!saveTestModel(model)) { testModelInput.value = state.testModel; log('测试模型不能为空', 'error'); return; } log(`已设置测试模型 ${state.testModel}`, 'success'); }); const autoDisableInput = root.querySelector('#sub2api-checker-auto-disable'); autoDisableInput.checked = state.autoDisable; autoDisableInput.addEventListener('change', () => { saveAutoDisable(autoDisableInput.checked); log(`模型异常时${state.autoDisable ? '会' : '不会'}自动关闭账号调度`, 'success'); }); root.querySelector('#sub2api-checker-refresh-group').addEventListener('click', () => { const group = readCurrentGroup(); if (group) { saveCurrentGroup(group, '当前页面'); } else { updateGroupDisplay(); log('未能从当前页面识别分组,开始时会弹窗确认', 'warn'); } }); root.querySelector('#sub2api-checker-start').addEventListener('click', () => run().catch((err) => { log(`运行异常:${err.message}`, 'error'); state.running = false; })); root.querySelector('#sub2api-checker-stop').addEventListener('click', () => { state.stopRequested = true; log('已请求停止,当前请求结束后退出', 'warn'); }); updateGroupDisplay(); updatePanelCollapsed(); } async function waitDomReady() { if (document.body) return; await new Promise((resolve) => { const timer = setInterval(() => { if (document.body) { clearInterval(timer); resolve(); } }, 50); }); } async function apiFetch(url, options = {}) { const headers = new Headers(options.headers || {}); if (state.authHeader && !headers.has('Authorization')) { headers.set('Authorization', state.authHeader); } const resp = await fetch(url, { ...options, headers, credentials: 'include', }); return resp; } async function fetchAccounts() { let page = 1; const items = []; while (true) { const url = new URL('/api/v1/admin/accounts', CONFIG.apiBase); url.searchParams.set('page', String(page)); url.searchParams.set('page_size', String(CONFIG.pageSize)); url.searchParams.set('platform', ''); url.searchParams.set('type', ''); url.searchParams.set('status', ''); url.searchParams.set('privacy_mode', ''); url.searchParams.set('group', state.currentGroup); url.searchParams.set('search', ''); url.searchParams.set('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Shanghai'); const resp = await apiFetch(url.toString(), { headers: { Accept: 'application/json, text/plain, */*' }, }); if (!resp.ok) throw new Error(`账号列表请求失败:HTTP ${resp.status}`); const json = await resp.json(); if (json.code !== 0) throw new Error(`账号列表返回异常:${json.message || json.code}`); const pageItems = json?.data?.items || []; items.push(...pageItems); const pages = Number(json?.data?.pages || 1); if (page >= pages || pageItems.length === 0) break; page += 1; } return items; } function getModels(account) { const targetModel = String(state.testModel || '').trim(); if (targetModel) return [targetModel]; const mapping = account?.credentials?.model_mapping || {}; const keys = Object.keys(mapping).filter(Boolean); if (keys.length <= 1) return keys; const preferred = []; for (const model of CONFIG.preferredModels) { if (keys.includes(model)) preferred.push(model); } const rest = keys.filter((k) => !preferred.includes(k)).sort(); return [...preferred, ...rest]; } async function testModel(accountId, modelId) { const controller = new AbortController(); let timer = null; const resetTimer = () => { clearTimeout(timer); timer = setTimeout(() => controller.abort(new Error(`模型 ${modelId} 流式超时`)), state.timeoutMs); }; try { resetTimer(); const resp = await apiFetch(`${CONFIG.apiBase}/api/v1/admin/accounts/${accountId}/test`, { method: 'POST', headers: { Accept: '*/*', 'Content-Type': 'application/json', }, body: JSON.stringify({ model_id: modelId, prompt: CONFIG.prompt }), signal: controller.signal, }); if (!resp.ok) { clearTimeout(timer); return { ok: false, reason: `HTTP ${resp.status}` }; } const reader = resp.body?.getReader(); if (!reader) { clearTimeout(timer); const text = await resp.text(); return { ok: false, reason: `无响应流:${text.slice(0, 200)}` }; } const decoder = new TextDecoder(); let buffer = ''; while (true) { resetTimer(); const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }).replace(/\r/g, ''); let splitIndex; while ((splitIndex = buffer.indexOf('\n\n')) >= 0) { const chunk = buffer.slice(0, splitIndex); buffer = buffer.slice(splitIndex + 2); const dataLines = chunk .split('\n') .map((line) => line.trim()) .filter((line) => line.startsWith('data:')) .map((line) => line.slice(5).trim()); for (const line of dataLines) { if (!line) continue; let event; try { event = JSON.parse(line); } catch (_) { continue; } if (event.type === 'error') { clearTimeout(timer); return { ok: false, reason: event.error || '未知错误' }; } if (event.type === 'test_complete') { clearTimeout(timer); return { ok: !!event.success, reason: event.success ? 'success' : 'test_complete=false' }; } } } } clearTimeout(timer); return { ok: false, reason: '响应流结束但没有 test_complete' }; } catch (err) { clearTimeout(timer); return { ok: false, reason: err?.name === 'AbortError' ? '请求超时' : (err?.message || String(err)), }; } } async function setAccountSchedulable(accountId, schedulable) { const resp = await apiFetch(`${CONFIG.apiBase}/api/v1/admin/accounts/${accountId}/schedulable`, { method: 'POST', headers: { Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/json', }, body: JSON.stringify({ schedulable: !!schedulable }), }); if (!resp.ok) { return { ok: false, reason: `HTTP ${resp.status}` }; } const json = await resp.json(); if (json.code !== 0) { return { ok: false, reason: json.message || `code=${json.code}` }; } return { ok: true, data: json.data }; } function resetStats() { state.stats = { total: 0, checked: 0, active: 0, started: 0, ok: 0, enabled: 0, disabled: 0, skipped: 0, failed: 0, }; updateStats(); const logBox = document.querySelector('#sub2api-checker-log'); if (logBox) logBox.innerHTML = ''; } async function ensureAuth() { const cached = getCachedAuthToken(); if (cached) { saveAuth(cached); return true; } if (state.authHeader) return true; const fromInput = document.querySelector('#sub2api-checker-auth')?.value?.trim(); if (fromInput) { saveAuth(fromInput); return true; } const manual = prompt('没有自动捕获到 Authorization,请粘贴 Bearer token'); if (!manual) return false; saveAuth(manual.trim()); return true; } async function ensureGroup() { const detected = readCurrentGroup(); if (detected) { return saveCurrentGroup(detected, '当前页面'); } if (state.currentGroup) return true; const lastGroup = normalizeGroup(localStorage.getItem(CONFIG.currentGroupStorageKey)); const manual = prompt( `没有识别到当前页面选择的分组,请填写本次测活分组。${lastGroup ? `\n上次识别到:${lastGroup}` : ''}`, lastGroup || '' ); if (!manual) return false; return saveCurrentGroup(manual.trim(), '手动确认'); } async function processAccount(account) { const title = `#${account.id} ${account.name || '(未命名)'}`; try { if (CONFIG.onlyCheckSchedulable && !account.schedulable) { state.stats.skipped += 1; log(`${title} 已去掉调度,跳过测试`, 'warn'); return; } const models = getModels(account); if (!models.length) { state.stats.failed += 1; if (!state.autoDisable) { log(`${title} 没有 model_mapping,但未关闭 schedulable`, 'warn'); return; } log(`${title} 没有 model_mapping,准备关闭`, 'error'); const off = await setAccountSchedulable(account.id, false); if (off.ok) { state.stats.disabled += 1; log(`${title} 已关闭 schedulable`, 'success'); } else { log(`${title} 关闭失败:${off.reason}`, 'error'); } return; } log(`${title} 开始测试 ${models.length} 个模型`); let accountOk = true; let failReason = ''; let testedCount = 0; for (const model of models) { if (state.stopRequested) break; log(`${title} 测试模型 ${model}`); const result = await testModel(account.id, model); testedCount += 1; if (!result.ok) { accountOk = false; failReason = `模型 ${model} 异常:${result.reason}`; log(`${title} ${failReason}`, 'error'); if (CONFIG.stopOnFirstModelFailure) break; } else { log(`${title} 模型 ${model} 正常`, 'success'); } } if (state.stopRequested && testedCount < models.length && accountOk) { state.stats.skipped += 1; log(`${title} 因停止请求未完成全部模型测试,未改动 schedulable`, 'warn'); return; } if (accountOk) { state.stats.ok += 1; if (!account.schedulable) { const on = await setAccountSchedulable(account.id, true); if (on.ok) { state.stats.enabled += 1; log(`${title} 全部模型正常,已重新启用 schedulable`, 'success'); } else { log(`${title} 模型正常但重新启用失败:${on.reason}`, 'error'); } } else { log(`${title} 全部模型正常`, 'success'); } } else { state.stats.failed += 1; if (!state.autoDisable) { log(`${title} 检测到异常但未关闭 schedulable(原因:${failReason})`, 'warn'); return; } const off = await setAccountSchedulable(account.id, false); if (off.ok) { state.stats.disabled += 1; log(`${title} 已关闭 schedulable(原因:${failReason})`, 'success'); } else { log(`${title} 关闭失败:${off.reason}`, 'error'); } } } finally { state.stats.checked += 1; updateStats(); } } async function runWorkerPool(accounts) { const concurrency = clampConcurrency(state.concurrency); log(`账号级并发 ${concurrency},单账号内模型按顺序测试`); async function worker(workerIndex) { while (!state.stopRequested) { const index = state.stats.started; if (index >= accounts.length) break; state.stats.started += 1; state.stats.active += 1; updateStats(); try { await processAccount(accounts[index]); } catch (err) { state.stats.failed += 1; state.stats.checked += 1; log(`工作线程 ${workerIndex} 处理账号异常:${err?.message || String(err)}`, 'error'); updateStats(); } finally { state.stats.active -= 1; updateStats(); } } } const workerCount = Math.min(concurrency, accounts.length); await Promise.all(Array.from({ length: workerCount }, (_, index) => worker(index + 1))); } async function run() { if (state.running) { log('已有任务在运行', 'warn'); return; } if (!(await ensureAuth())) { log('缺少 Authorization,已取消', 'error'); return; } if (!(await ensureGroup())) { log('缺少测活分组,已取消', 'error'); return; } state.running = true; state.stopRequested = false; resetStats(); try { state.collapsed = false; updatePanelCollapsed(); log(`开始拉取账号列表(分组:${state.currentGroup})`); const accounts = await fetchAccounts(); state.stats.total = accounts.length; updateStats(); log(`共获取 ${accounts.length} 个账号`, 'success'); await runWorkerPool(accounts); if (state.stopRequested) { log('任务已按要求停止', 'warn'); } else { log('巡检完成', 'success'); } } finally { state.running = false; state.stats.active = 0; updateStats(); } } injectAuthSniffer(); waitDomReady().then(() => { ensurePanel(); const group = readCurrentGroup(); if (group) saveCurrentGroup(group, '当前页面'); if (state.authHeader) { log('脚本已就绪,已从本地缓存 auth_token 读取 Authorization', 'success'); } else { log('脚本已就绪,未发现 auth_token;可刷新页面自动捕获或手动粘贴'); } }); })(); 1 个帖子 - 1 位参与者 阅读完整话题