2025年12月

之前书签管理用的是 raindrop,但是 raindrop 的搜索和没有一样。就试了下 karakeep,虽然不轻量,但是界面美观,配合 singlefile 可以持久化大部分网页(避免网页失效的问题),还有 ai 自动打标签移动端,总体还是挺好用的。

但是 raindrop 有个很好用的功能在 karakeep 没有:地址栏搜索,在浏览器的地址栏输入”rd xxx” 就可以搜索书签。karakeep 不仅没有这个功能,chrome 插件甚至不提供 sidebar,必须打开 karakeep 网页才能查看 or 搜索书签。

于是,我让 gpt 写了一个插件,来实现 karakeep 的地址栏搜索功能,地址栏输入”kk xxx” 搜索书签

Karakeep 快速搜索:只需地址栏输入 ‘kk’,即可搜索你的书签2

karakeep-omnibox-ext.zip

使用 karakeep 的佬友可以试试这个插件。
karakeep 是自搭建服务,所有有想试试的佬友也可以找我加个账号试试


📌 转载信息
原作者:
kg123456
转载时间:
2025/12/30 10:30:13

可以完成节点按照地区分区并且 AI 服务分流到 AI 分组,流媒体分流到流媒体分组

// 按地区分组脚本 - 支持负载均衡、自动选择、故障转移、流媒体和AI服务
function main(config) {
  // 国内直连规则
  const directRules = [
    "DOMAIN-SUFFIX,cn,DIRECT",
    "DOMAIN-SUFFIX,baidu.com,DIRECT",
    "DOMAIN-SUFFIX,bdstatic.com,DIRECT",
    "DOMAIN-SUFFIX,qq.com,DIRECT",
    "DOMAIN-SUFFIX,weixin.qq.com,DIRECT",
    "DOMAIN-SUFFIX,wechat.com,DIRECT",
    "DOMAIN-SUFFIX,taobao.com,DIRECT",
    "DOMAIN-SUFFIX,tmall.com,DIRECT",
    "DOMAIN-SUFFIX,alicdn.com,DIRECT",
    "DOMAIN-SUFFIX,alipay.com,DIRECT",
    "DOMAIN-SUFFIX,aliyun.com,DIRECT",
    "DOMAIN-SUFFIX,jd.com,DIRECT",
    "DOMAIN-SUFFIX,163.com,DIRECT",
    "DOMAIN-SUFFIX,126.com,DIRECT",
    "DOMAIN-SUFFIX,netease.com,DIRECT",
    "DOMAIN-SUFFIX,weibo.com,DIRECT",
    "DOMAIN-SUFFIX,sina.com.cn,DIRECT",
    "DOMAIN-SUFFIX,douyin.com,DIRECT",
    "DOMAIN-SUFFIX,tiktokv.com,DIRECT",
    "DOMAIN-SUFFIX,bytedance.com,DIRECT",
    "DOMAIN-SUFFIX,zhihu.com,DIRECT",
    "DOMAIN-SUFFIX,bilibili.com,DIRECT",
    "DOMAIN-SUFFIX,bilivideo.com,DIRECT",
    "DOMAIN-SUFFIX,hdslb.com,DIRECT",
    "DOMAIN-SUFFIX,douban.com,DIRECT",
    "DOMAIN-SUFFIX,xiaohongshu.com,DIRECT",
    "DOMAIN-SUFFIX,meituan.com,DIRECT",
    "DOMAIN-SUFFIX,dianping.com,DIRECT",
    "DOMAIN-SUFFIX,ctrip.com,DIRECT",
    "DOMAIN-SUFFIX,csdn.net,DIRECT",
    "DOMAIN-SUFFIX,jianshu.com,DIRECT",
    "DOMAIN-SUFFIX,iqiyi.com,DIRECT",
    "DOMAIN-SUFFIX,youku.com,DIRECT",
    "DOMAIN-SUFFIX,sohu.com,DIRECT",
    "DOMAIN-SUFFIX,sogou.com,DIRECT",
    "DOMAIN-SUFFIX,360.cn,DIRECT",
    "DOMAIN-SUFFIX,huawei.com,DIRECT",
    "DOMAIN-SUFFIX,xiaomi.com,DIRECT",
    "DOMAIN-SUFFIX,mi.com,DIRECT",
    "DOMAIN-SUFFIX,oppo.com,DIRECT",
    "DOMAIN-SUFFIX,vivo.com,DIRECT",
    "GEOIP,CN,DIRECT",
  ];

  // 流媒体和AI服务规则
  const serviceRules = [
    // AI 服务
    "DOMAIN-SUFFIX,openai.com,🤖 AI服务",
    "DOMAIN-SUFFIX,ai.com,🤖 AI服务",
    "DOMAIN-SUFFIX,chatgpt.com,🤖 AI服务",
    "DOMAIN-SUFFIX,oaistatic.com,🤖 AI服务",
    "DOMAIN-SUFFIX,oaiusercontent.com,🤖 AI服务",
    "DOMAIN-SUFFIX,gemini.google.com,🤖 AI服务",
    "DOMAIN-SUFFIX,bard.google.com,🤖 AI服务",
    "DOMAIN-SUFFIX,generativelanguage.googleapis.com,🤖 AI服务",
    "DOMAIN-SUFFIX,anthropic.com,🤖 AI服务",
    "DOMAIN-SUFFIX,claude.ai,🤖 AI服务",
    "DOMAIN-SUFFIX,x.ai,🤖 AI服务",
    "DOMAIN-SUFFIX,grok.x.ai,🤖 AI服务",
    "DOMAIN-SUFFIX,perplexity.ai,🤖 AI服务",
    "DOMAIN-SUFFIX,poe.com,🤖 AI服务",
    "DOMAIN-SUFFIX,cohere.ai,🤖 AI服务",
    "DOMAIN-SUFFIX,mistral.ai,🤖 AI服务",
    // 流媒体
    "DOMAIN-SUFFIX,netflix.com,🎬 流媒体",
    "DOMAIN-SUFFIX,netflix.net,🎬 流媒体",
    "DOMAIN-SUFFIX,nflxvideo.net,🎬 流媒体",
    "DOMAIN-SUFFIX,nflximg.net,🎬 流媒体",
    "DOMAIN-SUFFIX,nflxext.com,🎬 流媒体",
    "DOMAIN-SUFFIX,disneyplus.com,🎬 流媒体",
    "DOMAIN-SUFFIX,disney-plus.net,🎬 流媒体",
    "DOMAIN-SUFFIX,hbomax.com,🎬 流媒体",
    "DOMAIN-SUFFIX,max.com,🎬 流媒体",
    "DOMAIN-SUFFIX,hulu.com,🎬 流媒体",
    "DOMAIN-SUFFIX,primevideo.com,🎬 流媒体",
    "DOMAIN-SUFFIX,youtube.com,🎬 流媒体",
    "DOMAIN-SUFFIX,googlevideo.com,🎬 流媒体",
    "DOMAIN-SUFFIX,ytimg.com,🎬 流媒体",
    "DOMAIN-SUFFIX,spotify.com,🎬 流媒体",
    "DOMAIN-SUFFIX,scdn.co,🎬 流媒体",
    "DOMAIN-SUFFIX,tvb.com,🎬 流媒体",
    "DOMAIN-SUFFIX,viu.com,🎬 流媒体",
  ];

  // 地区分组正则 - 增强匹配规则
  const regionMap = {
    "🇭🇰 港澳台": /香港|HK|HongKong|Hong\s*Kong|港|🇭🇰|澳门|澳門|MO|Macao|Macau|🇲🇴|台湾|台灣|TW|Taiwan|臺灣|🇹🇼|高雄|台北/i,
    "🌎 美洲": /美国|美國|US|USA|United\s*States|🇺🇸|洛杉矶|洛杉磯|Los\s*Angeles|纽约|紐約|New\s*York|西雅图|Seattle|芝加哥|Chicago|达拉斯|Dallas|凤凰城|Phoenix|加拿大|CA|Canada|🇨🇦|多伦多|Toronto|温哥华|Vancouver|巴西|BR|Brazil|🇧🇷|圣保罗|Sao\s*Paulo|墨西哥|MX|Mexico|🇲🇽|墨西哥城|Mexico\s*City|阿根廷|AR|Argentina|🇦🇷|布宜诺斯艾利斯|Buenos\s*Aires|智利|CL|Chile|🇨🇱|圣地亚哥|Santiago|哥伦比亚|CO|Colombia|🇨🇴|波哥大|Bogota|秘鲁|PE|Peru|🇵🇪|利马|Lima/i,
    "🌍 欧洲": /英国|英國|GB|UK|United\s*Kingdom|Britain|🇬🇧|伦敦|倫敦|London|法国|法國|FR|France|🇫🇷|巴黎|Paris|德国|德國|DE|Germany|🇩🇪|法兰克福|Frankfurt|柏林|Berlin|意大利|IT|Italy|🇮🇹|罗马|Roma|米兰|Milan|西班牙|ES|Spain|🇪🇸|马德里|Madrid|巴塞罗那|Barcelona|荷兰|荷蘭|NL|Netherlands|🇳🇱|阿姆斯特丹|Amsterdam|瑞士|CH|Switzerland|🇨🇭|苏黎世|Zurich|瑞典|SE|Sweden|🇸🇪|斯德哥尔摩|Stockholm|波兰|波蘭|PL|Poland|🇵🇱|华沙|Warsaw|乌克兰|烏克蘭|UA|Ukraine|🇺🇦|基辅|Kiev|俄罗斯|俄羅斯|RU|Russia|🇷🇺|莫斯科|Moscow|葡萄牙|PT|Portugal|🇵🇹|里斯本|Lisbon|比利时|比利時|BE|Belgium|🇧🇪|布鲁塞尔|Brussels|奥地利|奧地利|AT|Austria|🇦🇹|维也纳|Vienna|挪威|NO|Norway|🇳🇴|奥斯陆|Oslo|丹麦|丹麥|DK|Denmark|🇩🇰|哥本哈根|Copenhagen|芬兰|芬蘭|FI|Finland|🇫🇮|赫尔辛基|Helsinki|爱尔兰|愛爾蘭|IE|Ireland|🇮🇪|都柏林|Dublin|捷克|CZ|Czech|🇨🇿|布拉格|Prague|罗马尼亚|羅馬尼亞|RO|Romania|🇷🇴|布加勒斯特|Bucharest|保加利亚|保加利亞|BG|Bulgaria|🇧🇬|索非亚|Sofia|希腊|希臘|GR|Greece|🇬🇷|雅典|Athens/i,
    "🌏 亚太": /日本|日|JP|Japan|🇯🇵|东京|東京|Tokyo|大阪|Osaka|新加坡|狮城|SG|Singapore|🇸🇬|韩国|韓國|KR|Korea|🇰🇷|首尔|首爾|Seoul|印度|IN|India|🇮🇳|孟买|Mumbai|新德里|New\s*Delhi|班加罗尔|Bangalore|澳大利亚|澳洲|AU|Australia|🇦🇺|悉尼|Sydney|墨尔本|Melbourne|新西兰|新西蘭|NZ|New\s*Zealand|🇳🇿|奥克兰|Auckland|马来西亚|馬來西亞|MY|Malaysia|🇲🇾|吉隆坡|Kuala\s*Lumpur|印度尼西亚|印尼|ID|Indonesia|🇮🇩|雅加达|Jakarta|菲律宾|菲律賓|PH|Philippines|🇵🇭|马尼拉|Manila|泰国|泰國|TH|Thailand|🇹🇭|曼谷|Bangkok|越南|VN|Vietnam|🇻🇳|胡志明|Ho\s*Chi\s*Minh|河内|Hanoi|巴基斯坦|PK|Pakistan|🇵🇰|卡拉奇|Karachi|孟加拉|孟加拉国|BD|Bangladesh|🇧🇩|达卡|Dhaka|斯里兰卡|LK|Sri\s*Lanka|🇱🇰|科伦坡|Colombo|缅甸|緬甸|MM|Myanmar|Burma|🇲🇲|仰光|Yangon|柬埔寨|KH|Cambodia|🇰🇭|金边|Phnom\s*Penh|老挝|老撾|LA|Laos|🇱🇦|万象|Vientiane/i,
    "🌍 中东非洲": /阿联酋|阿聯酋|UAE|Dubai|迪拜|🇦🇪|阿布扎比|Abu\s*Dhabi|以色列|IL|Israel|🇮🇱|特拉维夫|Tel\s*Aviv|南非|ZA|South\s*Africa|🇿🇦|开普敦|Cape\s*Town|约翰内斯堡|Johannesburg|尼日利亚|尼日利亞|NG|Nigeria|🇳🇬|拉各斯|Lagos|土耳其|TR|Turkey|Türkiye|🇹🇷|伊斯坦布尔|Istanbul|安卡拉|Ankara|沙特|沙特阿拉伯|SA|Saudi|🇸🇦|利雅得|Riyadh|吉达|Jeddah|卡塔尔|卡塔爾|QA|Qatar|🇶🇦|多哈|Doha|科威特|KW|Kuwait|🇰🇼|巴林|BH|Bahrain|🇧🇭|麦纳麦|Manama|阿曼|OM|Oman|🇴🇲|马斯喀特|Muscat|约旦|JO|Jordan|🇯🇴|安曼|Amman|埃及|EG|Egypt|🇪🇬|开罗|Cairo|肯尼亚|肯尼亞|KE|Kenya|🇰🇪|内罗毕|Nairobi|埃塞俄比亚|埃塞俄比亞|ET|Ethiopia|🇪🇹|亚的斯亚贝巴|Addis\s*Ababa|摩洛哥|MA|Morocco|🇲🇦|卡萨布兰卡|Casablanca|阿尔及利亚|阿爾及利亞|DZ|Algeria|🇩🇿|阿尔及尔|Algiers/i,
  };

  const excludePattern = /剩余|重置|更新|订阅|失联|超时|卡顿|飞行|续费/;
  const proxies = config.proxies || [];
  const validProxies = proxies.filter(p => !excludePattern.test(p.name));
  const proxyNames = validProxies.map(p => p.name);

  const regionGroups = [];
  const regionNames = [];

  for (const [name, regex] of Object.entries(regionMap)) {
    const matched = proxyNames.filter(n => regex.test(n));
    if (matched.length > 0) {
      const baseName = name.split(" ")[1];
      const autoName = `⚡ ${baseName}自动`;
      const lbName = `⚖️ ${baseName}均衡`;
      const fbName = `🔄 ${baseName}故障转移`;

      regionGroups.push(
        { name, type: "select", proxies: [autoName, lbName, fbName, ...matched] },
        { name: autoName, type: "url-test", proxies: matched, url: "http://www.gstatic.com/generate_204", interval: 300, tolerance: 50 },
        { name: lbName, type: "load-balance", proxies: matched, url: "http://www.gstatic.com/generate_204", interval: 300, strategy: "consistent-hashing" },
        { name: fbName, type: "fallback", proxies: matched, url: "http://www.gstatic.com/generate_204", interval: 180 }
      );
      regionNames.push(name);
    }
  }

  // AI服务分组 - 美洲+亚太节点
  const aiRegex = /美国|US|洛杉矶|纽约|日本|JP|Japan|新加坡|SG/i;
  const aiProxies = proxyNames.filter(n => aiRegex.test(n)).slice(0, 30);

  // 流媒体分组 - 亚太节点
  const streamRegex = /香港|HK|港|台湾|TW|新加坡|SG|日本|JP|Japan/i;
  const streamProxies = proxyNames.filter(n => streamRegex.test(n)).slice(0, 30);

  // 在节点选择组中添加分组(AI和流媒体在最前面)
  const groups = config["proxy-groups"] || [];
  const selectGroup = groups.find(g => g.name === "🚀 节点选择");
  if (selectGroup) {
    const directIdx = selectGroup.proxies.indexOf("DIRECT");
    const newGroups = ["🤖 AI服务", "🎬 流媒体", ...regionNames];
    if (directIdx > 0) {
      selectGroup.proxies.splice(directIdx, 0, ...newGroups);
    } else {
      selectGroup.proxies.push(...newGroups);
    }
  }

  // 添加所有分组
  config["proxy-groups"].push(...regionGroups);

  if (aiProxies.length > 0) {
    config["proxy-groups"].push(
      { name: "🤖 AI服务", type: "select", proxies: ["⚡ AI自动", "⚖️ AI均衡", "🔄 AI故障转移", ...aiProxies] },
      { name: "⚡ AI自动", type: "url-test", proxies: aiProxies, url: "http://www.gstatic.com/generate_204", interval: 300, tolerance: 100 },
      { name: "⚖️ AI均衡", type: "load-balance", proxies: aiProxies, url: "http://www.gstatic.com/generate_204", interval: 300, strategy: "consistent-hashing" },
      { name: "🔄 AI故障转移", type: "fallback", proxies: aiProxies, url: "http://www.gstatic.com/generate_204", interval: 180 }
    );
  }

  if (streamProxies.length > 0) {
    config["proxy-groups"].push(
      { name: "🎬 流媒体", type: "select", proxies: ["⚡ 流媒体自动", "⚖️ 流媒体均衡", "🔄 流媒体故障转移", ...streamProxies] },
      { name: "⚡ 流媒体自动", type: "url-test", proxies: streamProxies, url: "http://www.gstatic.com/generate_204", interval: 300, tolerance: 100 },
      { name: "⚖️ 流媒体均衡", type: "load-balance", proxies: streamProxies, url: "http://www.gstatic.com/generate_204", interval: 300, strategy: "consistent-hashing" },
      { name: "🔄 流媒体故障转移", type: "fallback", proxies: streamProxies, url: "http://www.gstatic.com/generate_204", interval: 180 }
    );
  }

  // 插入规则:服务规则在前,直连规则在后
  const rules = config.rules || [];
  config.rules = [...serviceRules, ...rules, ...directRules];

  return config;
}


📌 转载信息
原作者:
renhao20050625
转载时间:
2025/12/30 10:29:20

Notable changes
Implemented support for SSO with OpenID Connect, https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect
Updated web vault to 2025.12.0
Added support for future mobile apps with versions 2026.1.0+
This is the first vaultwarden release using immutable releases and release attestation!

更多信息参照:

机翻版本

显著变化


📌 转载信息
原作者:
preacher
转载时间:
2025/12/30 10:27:57

Google 向来低调,即便是 Gemini 3 这种顶级产品的发布,也不像其他公司一样搞得那么隆重,有一些小产品更是低调到发了很久都没人知道,比如这个面向程序员的产品 CodeWiki

和 Code Wiki 属于同一类产品,功能完全一致。

DeepWiki 底层可能采用了多家大模型,具体是哪个不太清楚,但是 CodeWiki 是基于 Gemini 模型的,Gemini 具有超长上下文,也就决定了 CodeWiki 非常适合做大型项目的代码分析

你只需要把 GitHub 仓库地址扔给它,它自动帮你写好这个项目的详细解析文档,甚至超过绝大多数仓库自带的 README 文档


📌 转载信息
转载时间:
2025/12/30 10:27:37

最近闲的没事,找了个项目(github 上的 Hello-Agents)看看,太久没看书了,都快阅读障碍了,还好有 Gemini 陪伴

Gemini 的活人感还是很足的,情绪价值也给的蛮足滴。课后习题也通通交给 Gemini


📌 转载信息
原作者:
St4rry
转载时间:
2025/12/30 10:27:16

此 docker 是在 perplexity-ai 基础上,增加了 mcp 功能,具体看上一个帖子。目前原项目没做自动更新 token 功能,token 一个月过期。
除了 mcp 之外的问题,提 issue 请找原项目

从这里继续讨论,https://linux.do/t/topic/1371904 增加 http 远程调用,因此封装了个 docker 自用,发出来给需要的人。

github workflow 自动化构建还没时间搞(让 ai 写了一版,没改变量,也没测)

docker compose 一键部署

注意,socks 代理没有测试过

services: perplexity-mcp:  shancw/perplexity-mcp:latest container_name: perplexity-mcp ports: - "${MCP_PORT:-8000}:8000" environment: # MCP 认证密钥 - MCP_TOKEN=${MCP_TOKEN:-sk-123456} # Perplexity 账户凭证 (可选,用于高级功能) - PPLX_NEXT_AUTH_CSRF_TOKEN=${PPLX_NEXT_AUTH_CSRF_TOKEN:-} - PPLX_SESSION_TOKEN=${PPLX_SESSION_TOKEN:-} # SOCKS 代理配置 (可选) # 格式: socks5://[user[:pass]@]host[:port][#remark] # 示例: socks5://127.0.0.1:1080 或 socks5://user:pass@proxy.example.com:1080 # - SOCKS_PROXY=${SOCKS_PROXY:-} restart: unless-stopped 

.env 环境变量

# Perplexity MCP Server 环境变量配置 # 复制此文件为 .env 并填入实际值 # ============================================ # MCP 服务配置 # ============================================ # MCP 服务端口
MCP_PORT=8000

# MCP API 认证密钥 (客户端需要在 Authorization header 中携带此密钥)
MCP_TOKEN=sk-123456

# ============================================ # Perplexity 账户凭证 (可选) # 用于解锁高级功能: Pro 模式、Reasoning 模式、Deep Research # 不配置则只能使用 auto 模式 # ============================================ # 从 Perplexity 网站 Cookie 中获取 # 打开 perplexity.ai -> F12 开发者工具 -> Application -> Cookies
PPLX_NEXT_AUTH_CSRF_TOKEN=
PPLX_SESSION_TOKEN=


perplexity mcp docker 一键部署(一个 pro 账号无限额度独享 ai 搜索 mcp)3

mcp 配置

{ "perplexity": { "type": "http", "url": "http://127.0.0.1:8000/mcp", "headers": { "Authorization": "Bearer sk-123456" } } } 

github


📌 转载信息
原作者:
shan_CW
转载时间:
2025/12/30 10:27:01

这是一本讲网页排版书籍,书中内容有在线示例与代码,是这么多年来对我极其有用的平面设计书籍之一。原书是英文,翻译了在 L 站分享给大家。

此书为 90% 的 AI 翻译,完全保留原书排版,由个人粗略校对确保大部分用词统一。

书名:Better Web Typography for a Better Web
译名:更好的网页排版,造就更好的互联网

推荐语

2018 年,我还在大学社团做海报,那时候对排版的理解更多是靠 “直觉”。直到接触到了 Matej Latin 的网页排版公开课,也就是这本书的起源。课和中提出 “排版的完美等边三角形”—— 即字号、行高与行宽之间的动态平衡,以及 “模块化比例” 的概念,彻底改变了我观察网页的视角。我至今铭记在心:“要获得完美的段落,需要三样东西:字体大小、行高和行宽。它们需要达到平衡。”

从那以后,这三要素就成了我做阅读文字时绕不开的底层直觉。每当我看到一个网页,总会下意识地去拆解它的文字比例是否协调。尽管此书以英文为基准,其中的排版规律对多种语言均适用。

虽然这本书写在八年前,在互联网平面设计风格迭代如此之快的今天,它讨论的核心规则却依然稳固。甚至可以说,如今主流的网页排版方向,就如书中的所言。如果你想摆脱 “凭感觉” 排版,真正理解排版的美学逻辑,倾情您阅读推荐这本书。

简介

Better Web Typography for a Better Web 是一本源自高分在线课程的著作,旨在向网页设计师和开发人员等网站构建者讲解排版。作者 Matej Latin 将垂直节奏(vertical rhythm)、模块化比例(modular scale)和页面构成(page composition)等复杂概念,以通俗易懂的方式进行了深入浅出的解析。本书配有实时代码示例,读者在阅读过程中将亲历设计并构建一个示例网站的完整流程。这是一本针对新媒介的排印新书:基本规则虽未改变,但除此之外的一切都已焕然一新。

资源链接

CloudFlare - R2 直链下载:

标签: 排版,字体排印,平面设计,网页设计


📌 转载信息
原作者:
illy
转载时间:
2025/12/30 10:24:04

又看到站内大佬的一款,太棒了!~

佬们,还有别的推荐吗?

给个赞呗~


📌 转载信息
原作者:
you_z
转载时间:
2025/12/30 10:23:43

全自动看网课 ((支持倍速))

这才是真正得解放了我的生产力,解决了某些网站没有脚本自动完成的难题

另外补一个 timer hooker 脚本(源自油猴某大佬,适用于所有网站加速)

// ==UserScript==
// @name            计时器掌控者|视频广告跳过|视频广告加速器
// @name:en         TimerHooker
// @namespace       https://gitee.com/HGJing/everthing-hook/
// @version         1.0.62
// @description     控制网页计时器速度|加速跳过页面计时广告|视频快进(慢放)|跳过广告|支持几乎所有网页.
// @description:en  it can hook the timer speed to change.
// @include         *
// @require         https://greasyfork.org/scripts/372672-everything-hook/code/Everything-Hook.js?version=881251
// @author          Cangshi
// @match           http://*/*
// @run-at          document-start
// @grant           none
// @license         GPL-3.0-or-later
// @downloadURL https://update.greasyfork.org/scripts/372673/%E8%AE%A1%E6%97%B6%E5%99%A8%E6%8E%8C%E6%8E%A7%E8%80%85%7C%E8%A7%86%E9%A2%91%E5%B9%BF%E5%91%8A%E8%B7%B3%E8%BF%87%7C%E8%A7%86%E9%A2%91%E5%B9%BF%E5%91%8A%E5%8A%A0%E9%80%9F%E5%99%A8.user.js
// @updateURL https://update.greasyfork.org/scripts/372673/%E8%AE%A1%E6%97%B6%E5%99%A8%E6%8E%8C%E6%8E%A7%E8%80%85%7C%E8%A7%86%E9%A2%91%E5%B9%BF%E5%91%8A%E8%B7%B3%E8%BF%87%7C%E8%A7%86%E9%A2%91%E5%B9%BF%E5%91%8A%E5%8A%A0%E9%80%9F%E5%99%A8.meta.js
// ==/UserScript==
/**
 * ---------------------------
 * Time: 2017/11/20 19:28.
 * Author: Cangshi
 * View: http://palerock.cn
 * ---------------------------
 */

/**
 * 1. hook Object.defineProperty | Object.defineProperties
 * 2. set configurable: true
 * 3. delete property
 * 4. can set property for onxx event method
 */

window.isDOMLoaded = false;
window.isDOMRendered = false;

document.addEventListener('readystatechange', function () {
    if (document.readyState === "interactive" || document.readyState === "complete") {
        window.isDOMLoaded = true;
    }
});

~function (global) {

    var workerURLs = [];
    var extraElements = [];
    var suppressEvents = {};

    var helper = function (eHookContext, timerContext, util) {
        return {
            applyUI: function () {
                var style = '._th-container ._th-item{margin-bottom:3px;position:relative;width:0;height:0;cursor:pointer;opacity:.3;background-color:aquamarine;border-radius:100%;text-align:center;line-height:30px;-webkit-transition:all .35s;-o-transition:all .35s;transition:all .35s;right:30px}._th-container ._th-item,._th-container ._th-click-hover,._th_cover-all-show-times ._th_times{-webkit-box-shadow:-3px 4px 12px -5px black;box-shadow:-3px 4px 12px -5px black}._th-container:hover ._th-item._item-x2{margin-left:18px;width:40px;height:40px;line-height:40px}._th-container:hover ._th-item._item-x-2{margin-left:17px;width:38px;height:38px;line-height:38px}._th-container:hover ._th-item._item-xx2{width:36px;height:36px;margin-left:16px;line-height:36px}._th-container:hover ._th-item._item-xx-2{width:32px;height:32px;line-height:32px;margin-left:14px}._th-container:hover ._th-item._item-reset{width:30px;line-height:30px;height:30px;margin-left:10px}._th-click-hover{position:relative;-webkit-transition:all .5s;-o-transition:all .5s;transition:all .5s;height:45px;width:45px;cursor:pointer;opacity:.3;border-radius:100%;background-color:aquamarine;text-align:center;line-height:45px;right:0}._th-container:hover{left:-5px}._th-container{font-size:12px;-webkit-transition:all .5s;-o-transition:all .5s;transition:all .5s;left:-35px;top:20%;position:fixed;-webkit-box-sizing:border-box;box-sizing:border-box;z-index:100000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}._th-container ._th-item:hover{opacity:.8;background-color:#5fb492;color:aliceblue}._th-container ._th-item:active{opacity:.9;background-color:#1b3a26;color:aliceblue}._th-container:hover ._th-click-hover{opacity:.8}._th-container:hover ._th-item{opacity:.6;right:0}._th-container ._th-click-hover:hover{opacity:.8;background-color:#5fb492;color:aliceblue}._th_cover-all-show-times{position:fixed;top:0;right:0;width:100%;height:100%;z-index:99999;opacity:1;font-weight:900;font-size:30px;color:#4f4f4f;background-color:rgba(0,0,0,0.1)}._th_cover-all-show-times._th_hidden{z-index:-99999;opacity:0;-webkit-transition:1s all;-o-transition:1s all;transition:1s all}._th_cover-all-show-times ._th_times{width:300px;height:300px;border-radius:50%;background-color:rgba(127,255,212,0.51);text-align:center;line-height:300px;position:absolute;top:50%;right:50%;margin-top:-150px;margin-right:-150px}';

                var displayNum = (1 / timerContext._percentage).toFixed(2);

                // 在页面左边添加一个半圆便于修改
                var html = '<div class="_th-container">\n' +
                    '    <div class="_th-click-hover _item-input">\n' +
                    '        x' + displayNum + '\n' +
                    '    </div>\n' +
                    '    <div class="_th-item _item-x2">&gt;</div>\n' +
                    '    <div class="_th-item _item-x-2">&lt;</div>\n' +
                    '    <div class="_th-item _item-xx2">&gt;&gt;</div>\n' +
                    '    <div class="_th-item _item-xx-2">&lt;&lt;</div>\n' +
                    '    <div class="_th-item _item-reset">O</div>\n' +
                    '</div>\n' +
                    '<div class="_th_cover-all-show-times _th_hidden">\n' +
                    '    <div class="_th_times">x' + displayNum + '</div>\n' +
                    '</div>' +
                    '';
                var stylenode = document.createElement('style');
                stylenode.setAttribute("type", "text/css");
                if (stylenode.styleSheet) {// IE
                    stylenode.styleSheet.cssText = style;
                } else {// w3c
                    var cssText = document.createTextNode(style);
                    stylenode.appendChild(cssText);
                }
                var node = document.createElement('div');
                node.innerHTML = html;

                var clickMapper = {
                    '_item-input': function () {
                        changeTime();
                    },
                    '_item-x2': function () {
                        changeTime(2, 0, true);
                    },
                    '_item-x-2': function () {
                        changeTime(-2, 0, true);
                    },
                    '_item-xx2': function () {
                        changeTime(0, 2);
                    },
                    '_item-xx-2': function () {
                        changeTime(0, -2);
                    },
                    '_item-reset': function () {
                        changeTime(0, 0, false, true);
                    }
                };

                Object.keys(clickMapper).forEach(function (className) {
                    var exec = clickMapper[className];
                    var targetEle = node.getElementsByClassName(className)[0];
                    if (targetEle) {
                        targetEle.onclick = exec;
                    }
                });

                if (!global.isDOMLoaded) {
                    document.addEventListener('readystatechange', function () {
                        if ((document.readyState === "interactive" || document.readyState === "complete") && !global.isDOMRendered) {
                            document.head.appendChild(stylenode);
                            document.body.appendChild(node);
                            global.isDOMRendered = true;
                            console.log('Time Hooker Works!');
                        }
                    });
                } else {
                    document.head.appendChild(stylenode);
                    document.body.appendChild(node);
                    global.isDOMRendered = true;
                    console.log('Time Hooker Works!');
                }
            },
            applyGlobalAction: function (timer) {
                // 界面半圆按钮点击的方法
                timer.changeTime = function (anum, cnum, isa, isr) {
                    if (isr) {
                        global.timer.change(1);
                        return;
                    }
                    if (!global.timer) {
                        return;
                    }
                    var result;
                    if (!anum && !cnum) {
                        var t = prompt("输入欲改变计时器变化倍率(当前:" + 1 / timerContext._percentage + ")");
                        if (t == null) {
                            return;
                        }
                        if (isNaN(parseFloat(t))) {
                            alert("请输入正确的数字");
                            timer.changeTime();
                            return;
                        }
                        if (parseFloat(t) <= 0) {
                            alert("倍率不能小于等于0");
                            timer.changeTime();
                            return;
                        }
                        result = 1 / parseFloat(t);
                    } else {
                        if (isa && anum) {
                            if (1 / timerContext._percentage <= 1 && anum < 0) {
                                return;
                            }
                            result = 1 / (1 / timerContext._percentage + anum);
                        } else {
                            if (cnum <= 0) {
                                cnum = 1 / -cnum
                            }
                            result = 1 / ((1 / timerContext._percentage) * cnum);
                        }
                    }
                    timer.change(result);
                };
                global.changeTime = timer.changeTime;
            },
            applyHooking: function () {
                var _this = this;
                // 劫持循环计时器
                eHookContext.hookReplace(window, 'setInterval', function (setInterval) {
                    return _this.getHookedTimerFunction('interval', setInterval);
                });
                // 劫持单次计时
                eHookContext.hookReplace(window, 'setTimeout', function (setTimeout) {
                    return _this.getHookedTimerFunction('timeout', setTimeout)
                });
                // 劫持循环计时器的清除方法
                eHookContext.hookBefore(window, 'clearInterval', function (method, args) {
                    _this.redirectNewestId(args);
                });
                // 劫持循环计时器的清除方法
                eHookContext.hookBefore(window, 'clearTimeout', function (method, args) {
                    _this.redirectNewestId(args);
                });
                var newFunc = this.getHookedDateConstructor();
                eHookContext.hookClass(window, 'Date', newFunc, '_innerDate', ['now']);
                Date.now = function () {
                    return new Date().getTime();
                };
                eHookContext.hookedToString(timerContext._Date.now, Date.now);
                var objToString = Object.prototype.toString;

                Object.prototype.toString = function toString() {
                    'use strict';
                    if (this instanceof timerContext._mDate) {
                        return '[object Date]';
                    } else {
                        return objToString.call(this);
                    }
                };

                eHookContext.hookedToString(objToString, Object.prototype.toString);
                eHookContext.hookedToString(timerContext._setInterval, setInterval);
                eHookContext.hookedToString(timerContext._setTimeout, setTimeout);
                eHookContext.hookedToString(timerContext._clearInterval, clearInterval);
                timerContext._mDate = window.Date;
                this.hookShadowRoot();
            },
            getHookedDateConstructor: function () {
                return function () {
                    if (arguments.length === 1) {
                        Object.defineProperty(this, '_innerDate', {
                            configurable: false,
                            enumerable: false,
                            value: new timerContext._Date(arguments[0]),
                            writable: false
                        });
                        return;
                    } else if (arguments.length > 1) {
                        var definedValue;
                        switch (arguments.length) {
                            case 2:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1]
                                );
                                break;
                            case 3:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1],
                                    arguments[2],
                                );
                                break;
                            case 4:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1],
                                    arguments[2],
                                    arguments[3],
                                );
                                break;
                            case 5:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1],
                                    arguments[2],
                                    arguments[3],
                                    arguments[4]
                                );
                                break;
                            case 6:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1],
                                    arguments[2],
                                    arguments[3],
                                    arguments[4],
                                    arguments[5]
                                );
                                break;
                            default:
                            case 7:
                                definedValue = new timerContext._Date(
                                    arguments[0],
                                    arguments[1],
                                    arguments[2],
                                    arguments[3],
                                    arguments[4],
                                    arguments[5],
                                    arguments[6]
                                );
                                break;
                        }

                        Object.defineProperty(this, '_innerDate', {
                            configurable: false,
                            enumerable: false,
                            value: definedValue,
                            writable: false
                        });
                        return;
                    }
                    var now = timerContext._Date.now();
                    var passTime = now - timerContext.__lastDatetime;
                    var hookPassTime = passTime * (1 / timerContext._percentage);
                    // console.log(__this.__lastDatetime + hookPassTime, now,__this.__lastDatetime + hookPassTime - now);
                    Object.defineProperty(this, '_innerDate', {
                        configurable: false,
                        enumerable: false,
                        value: new timerContext._Date(timerContext.__lastMDatetime + hookPassTime),
                        writable: false
                    });
                };
            },
            getHookedTimerFunction: function (type, timer) {
                var property = '_' + type + 'Ids';
                return function () {
                    var uniqueId = timerContext.genUniqueId();
                    var callback = arguments[0];
                    if (typeof callback === 'string') {
                        callback += ';timer.notifyExec(' + uniqueId + ')';
                        arguments[0] = callback;
                    }
                    if (typeof callback === 'function') {
                        arguments[0] = function () {
                            var returnValue = callback.apply(this, arguments);
                            timerContext.notifyExec(uniqueId);
                            return returnValue;
                        }
                    }
                    // 储存原始时间间隔
                    var originMS = arguments[1];
                    // 获取变速时间间隔
                    arguments[1] *= timerContext._percentage;
                    var resultId = timer.apply(window, arguments);
                    // 保存每次使用计时器得到的id以及参数等
                    timerContext[property][resultId] = {
                        args: arguments,
                        originMS: originMS,
                        originId: resultId,
                        nowId: resultId,
                        uniqueId: uniqueId,
                        oldPercentage: timerContext._percentage,
                        exceptNextFireTime: timerContext._Date.now() + originMS
                    };
                    return resultId;
                };
            },
            redirectNewestId: function (args) {
                var id = args[0];
                if (timerContext._intervalIds[id]) {
                    args[0] = timerContext._intervalIds[id].nowId;
                    // 清除该记录id
                    delete timerContext._intervalIds[id];
                }
                if (timerContext._timeoutIds[id]) {
                    args[0] = timerContext._timeoutIds[id].nowId;
                    // 清除该记录id
                    delete timerContext._timeoutIds[id];
                }
            },
            registerShortcutKeys: function (timer) {
                // 快捷键注册
                addEventListener('keydown', function (e) {
                    switch (e.keyCode) {
                        case 57:
                            if (e.ctrlKey || e.altKey) {
                                // custom
                                timer.changeTime();
                            }
                            break;
                        // [=]
                        case 190:
                        case 187: {
                            if (e.ctrlKey) {
                                // console.log('+2');
                                timer.changeTime(2, 0, true);
                            } else if (e.altKey) {
                                // console.log('xx2');
                                timer.changeTime(0, 2);
                            }
                            break;
                        }
                        // [-]
                        case 188:
                        case 189: {
                            if (e.ctrlKey) {
                                // console.log('-2');
                                timer.changeTime(-2, 0, true);
                            } else if (e.altKey) {
                                // console.log('xx-2');
                                timer.changeTime(0, -2);
                            }
                            break;
                        }
                        // [0]
                        case 48: {
                            if (e.ctrlKey || e.altKey) {
                                // console.log('reset');
                                timer.changeTime(0, 0, false, true);
                            }
                            break;
                        }
                        default:
                        // console.log(e);
                    }
                });
            },
            /**
             * 当计时器速率被改变时调用的回调方法
             * @param percentage
             * @private
             */
            percentageChangeHandler: function (percentage) {
                // 改变所有的循环计时
                util.ergodicObject(timerContext, timerContext._intervalIds, function (idObj, id) {
                    idObj.args[1] = Math.floor((idObj.originMS || 1) * percentage);
                    // 结束原来的计时器
                    this._clearInterval.call(window, idObj.nowId);
                    // 新开一个计时器
                    idObj.nowId = this._setInterval.apply(window, idObj.args);
                });
                // 改变所有的延时计时
                util.ergodicObject(timerContext, timerContext._timeoutIds, function (idObj, id) {
                    var now = this._Date.now();
                    var exceptTime = idObj.exceptNextFireTime;
                    var oldPercentage = idObj.oldPercentage;
                    var time = exceptTime - now;
                    if (time < 0) {
                        time = 0;
                    }
                    var changedTime = Math.floor(percentage / oldPercentage * time);
                    idObj.args[1] = changedTime;
                    // 重定下次执行时间
                    idObj.exceptNextFireTime = now + changedTime;
                    idObj.oldPercentage = percentage;
                    // 结束原来的计时器
                    this._clearTimeout.call(window, idObj.nowId);
                    // 新开一个计时器
                    idObj.nowId = this._setTimeout.apply(window, idObj.args);
                });
            },
            hookShadowRoot: function () {
                var origin = Element.prototype.attachShadow;
                eHookContext.hookAfter(Element.prototype, 'attachShadow',
                    function (m, args, result) {
                        extraElements.push(result);
                        return result;
                    }, false);
                eHookContext.hookedToString(origin, Element.prototype.attachShadow);
            },
            hookDefine: function () {
                const _this = this;
                eHookContext.hookBefore(Object, 'defineProperty', function (m, args) {
                    var option = args[2];
                    var ele = args[0];
                    var key = args[1];
                    var afterArgs = _this.hookDefineDetails(ele, key, option);
                    afterArgs.forEach((arg, i) => {
                        args[i] = arg;
                    })
                });
                eHookContext.hookBefore(Object, 'defineProperties', function (m, args) {
                    var option = args[1];
                    var ele = args[0];
                    if (ele && ele instanceof Element) {
                        Object.keys(option).forEach(key => {
                            var o = option[key];
                            var afterArgs = _this.hookDefineDetails(ele, key, o);
                            args[0] = afterArgs[0];
                            delete option[key];
                            option[afterArgs[1]] = afterArgs[2]
                        })
                    }
                })
            },
            hookDefineDetails: function (target, key, option) {
                if (option && target && target instanceof Element && typeof key === 'string' && key.indexOf('on') >= 0) {
                    option.configurable = true;
                }
                if (target instanceof HTMLVideoElement && key === 'playbackRate') {
                    option.configurable = true;
                    console.warn('[Timer Hook]', '已阻止默认操作视频倍率');
                    key = 'playbackRate_hooked'
                }
                return [target, key, option];
            },
            suppressEvent: function (ele, eventName) {
                if (ele) {
                    delete ele['on' + eventName];
                    delete ele['on' + eventName];
                    delete ele['on' + eventName];
                    ele['on' + eventName] = undefined;
                }
                if (!suppressEvents[eventName]) {
                    eHookContext.hookBefore(EventTarget.prototype, 'addEventListener',
                        function (m, args) {
                            var eName = args[0];
                            if (eventName === eName) {
                                console.warn(eventName, 'event suppressed.')
                                args[0] += 'suppressed';
                            }
                        }, false);
                    suppressEvents[eventName] = true;
                }
            },
            changePlaybackRate: function (ele, rate) {
                delete ele.playbackRate;
                delete ele.playbackRate;
                delete ele.playbackRate;
                ele.playbackRate = rate
                if (rate !== 1) {
                    timerContext.defineProperty.call(Object, ele, 'playbackRate', {
                        configurable: true,
                        get: function () {
                            return 1;
                        },
                        set: function () {
                        }
                    });
                }
            }
        }
    };

    var normalUtil = {
        isInIframe: function () {
            let is = global.parent !== global;
            try {
                is = is && global.parent.document.body.tagName !== 'FRAMESET'
            } catch (e) {
                // ignore
            }
            return is;
        },
        listenParentEvent: function (handler) {
            global.addEventListener('message', function (e) {
                var data = e.data;
                var type = data.type || '';
                if (type === 'changePercentage') {
                    handler(data.percentage || 0);
                }
            })
        },
        sentChangesToIframe: function (percentage) {
            var iframes = document.querySelectorAll('iframe') || [];
            var frames = document.querySelectorAll('frame');
            if (iframes.length) {
                for (var i = 0; i < iframes.length; i++) {
                    iframes[i].contentWindow.postMessage(
                        {type: 'changePercentage', percentage: percentage}, '*');
                }
            }
            if (frames.length) {
                for (var j = 0; j < frames.length; j++) {
                    frames[j].contentWindow.postMessage(
                        {type: 'changePercentage', percentage: percentage}, '*');
                }
            }
        }
    };

    var querySelectorAll = function (ele, selector, includeExtra) {
        var elements = ele.querySelectorAll(selector);
        elements = Array.prototype.slice.call(elements || []);
        if (includeExtra) {
            extraElements.forEach(function (element) {
                elements = elements.concat(querySelectorAll(element, selector, false));
            })
        }
        return elements;
    };

    var generate = function () {
        return function (util) {
            // disable worker
            workerURLs.forEach(function (url) {
                if (util.urlMatching(location.href, 'http.*://.*' + url + '.*')) {
                    window['Worker'] = undefined;
                    console.log('Worker disabled');
                }
            });
            var eHookContext = this;
            var timerHooker = {
                // 用于储存计时器的id和参数
                _intervalIds: {},
                _timeoutIds: {},
                _auoUniqueId: 1,
                // 计时器速率
                __percentage: 1.0,
                // 劫持前的原始的方法
                _setInterval: window['setInterval'],
                _clearInterval: window['clearInterval'],
                _clearTimeout: window['clearTimeout'],
                _setTimeout: window['setTimeout'],
                _Date: window['Date'],
                __lastDatetime: new Date().getTime(),
                __lastMDatetime: new Date().getTime(),
                videoSpeedInterval: 1000,
                defineProperty: Object.defineProperty,
                defineProperties: Object.defineProperties,
                genUniqueId: function () {
                    return this._auoUniqueId++;
                },
                notifyExec: function (uniqueId) {
                    var _this = this;
                    if (uniqueId) {
                        // 清除 timeout 所储存的记录
                        var timeoutInfos = Object.values(this._timeoutIds).filter(
                            function (info) {
                                return info.uniqueId === uniqueId;
                            }
                        );
                        timeoutInfos.forEach(function (info) {
                            _this._clearTimeout.call(window, info.nowId);
                            delete _this._timeoutIds[info.originId]
                        })
                    }
                    // console.log(uniqueId, 'called')
                },
                /**
                 * 初始化方法
                 */
                init: function () {
                    var timerContext = this;
                    var h = helper(eHookContext, timerContext, util);

                    h.hookDefine();
                    h.applyHooking();

                    // 设定百分比属性被修改的回调
                    Object.defineProperty(timerContext, '_percentage', {
                        get: function () {
                            return timerContext.__percentage;
                        },
                        set: function (percentage) {
                            if (percentage === timerContext.__percentage) {
                                return percentage;
                            }
                            h.percentageChangeHandler(percentage);
                            timerContext.__percentage = percentage;
                            return percentage;
                        }
                    });

                    if (!normalUtil.isInIframe()) {
                        console.log('[TimeHooker]', 'loading outer window...');
                        h.applyUI();
                        h.applyGlobalAction(timerContext);
                        h.registerShortcutKeys(timerContext);
                    } else {
                        console.log('[TimeHooker]', 'loading inner window...');
                        normalUtil.listenParentEvent((function (percentage) {
                            console.log('[TimeHooker]', 'Inner Changed', percentage)
                            this.change(percentage);
                        }).bind(this))
                    }
                },
                /**
                 * 调用该方法改变计时器速率
                 * @param percentage
                 */
                change: function (percentage) {
                    this.__lastMDatetime = this._mDate.now();
                    this.__lastDatetime = this._Date.now();
                    this._percentage = percentage;
                    var oldNode = document.getElementsByClassName('_th-click-hover');
                    var oldNode1 = document.getElementsByClassName('_th_times');
                    var displayNum = (1 / this._percentage).toFixed(2);
                    (oldNode[0] || {}).innerHTML = 'x' + displayNum;
                    (oldNode1[0] || {}).innerHTML = 'x' + displayNum;
                    var a = document.getElementsByClassName('_th_cover-all-show-times')[0] || {};
                    a.className = '_th_cover-all-show-times';
                    this._setTimeout.bind(window)(function () {
                        a.className = '_th_cover-all-show-times _th_hidden';
                    }, 100);
                    this.changeVideoSpeed();
                    normalUtil.sentChangesToIframe(percentage);
                },
                changeVideoSpeed: function () {
                    var timerContext = this;
                    var h = helper(eHookContext, timerContext, util);
                    var rate = 1 / this._percentage;
                    rate > 16 && (rate = 16);
                    rate < 0.065 && (rate = 0.065);
                    var videos = querySelectorAll(document, 'video', true) || [];
                    if (videos.length) {
                        for (var i = 0; i < videos.length; i++) {
                            h.changePlaybackRate(videos[i], rate);
                        }
                    }
                }
            };
            // 默认初始化
            timerHooker.init();
            return timerHooker;
        }
    };

    if (global.eHook) {
        global.eHook.plugins({
            name: 'timer',
            /**
             * 插件装载
             * @param util
             */
            mount: generate()
        });
    }
}(window);


📌 转载信息
原作者:
Jaxon-jp
转载时间:
2025/12/30 10:23:32

项目地址:GitHub - CandyMuj/DockerCN: 针对中国网络环境的 Docker 使用优化,解决 pull push login 的网络问题,优化镜像内 apt pip 等工具为国内源!支持多平台架构镜像构建!

已经优化的部分镜像:DockerHub - CandyMuj

现在部署项目基本都依赖于 Docker 了,但是国内的网络环境导致使用 Docker 很不方便

  • 看了下目前安装 Docker 的方式多种多样,但似乎都挺麻烦的,其实可以直接用如下简洁的命令一键安装(基于阿里云,这个似乎是很多年前在菜鸟教程看见的安装方式,我就一直用到了现在)

    不知为啥现在好多教程都搜不到这个命令了,刚刚看了下菜鸟教程的安装方式也不是这个命令了,但是这个命令确实还可以用的

    curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
    
  • docker pull docker login docker push 直接用不了

  • 当上面的问题解决后,在使用 Docker 镜像的时候也会遇到问题,比如 apt pip maven npm 等都存在网络问题

  • 并且我希望这个东西更通用,除了可以用于 Docker,也可以直接用于优化国内的服务器

  • 执行优化可以高度自定义(自己想怎么优化就怎么优化,相当于提供一个框架,可以自定义实现)

我不希望每次使用的时候还要手动处理镜像源相关的问题,我就在想有没有可能直接开箱即用,镜像拉下来就可以使用,想了想,自己造一个吧,于是就花了两天实际造了这个项目

具体的使用及优化细节,可以看看 README,自认为写的还是比较详细的(甚至可能有点啰嗦)

希望对佬友们有用!减少不必要的时间,提升效率!

摘自:项目简介

旨在解决在中国使用 Docker 及 Docker 镜像时的网络问题!加速镜像构建和依赖下载,节省使用 Docker 的时间,让 Docker 更丝滑!

  • 由于官方 DockerHub 被墙,导致 pull push login 无法使用
    1. 解决 pull 问题:配置 Docker 镜像源
    2. 解决 docker 所有命令的网络问题:配置 Docker 代理
  • 优化 Docker 镜像,解决镜像内 包管理器(apt、apk) Python Pip Java Maven 等工具或环境的默认源缓慢或被墙导致无法使用的问题
    1. 理论上支持所有镜像的优化
    2. 使用本仓库的脚本进行优化:将自动配置国内镜像源并重新构建镜像 快速开始
  • 可优化本地镜像,某些镜像未 push 到中央仓库,仅在服务器本地存在,此时可 clone 本项目脚本到服务器,进行本地化的优化 快速开始 - 本地镜像优化
  • 亦可直接优化服务器,而非仅局限于 Docker 镜像 直接优化服务器
  • 极低侵入性:优化不会对原镜像做任何的其他修改,仅优化该优化了,比如:不会修改原有的工作目录、原有的启动命令、不夹带私货 (不安装任何软件包) 等
  • 支持多平台架构镜像构建!

鸣谢

其中关于 Docker 代理的配置方法参考自这位佬友的帖子 请教下佬们 docker login 登录报错问题 - #10,来自 yhp666

我也是试了很多方法去配置代理,一直没成功,然后在站内搜了下发现了这个佬友的帖子


📌 转载信息
转载时间:
2025/12/30 10:23:14

佬友们,分享一下我最近开发的 Excel 智能数据分析软件–ExcelMind
算是在 L 站第一次分享我的开源项目,希望佬友们多多提 Issue,多多 Star。

下面是演示视频:

这个项目是基于 LangGraph 开发的,支持自然语言查询、多轮对话、流式输出、ECharts 图表可视化 和可视化思考过程。

GitHub 地址:GitHub - stark-456/ExcelMind: AI 智能分析 Excel 文件,对话式完成多场景 Excel 分析任务,解决 Excel 报表分析复杂、效率低等痛点

上传 Excel 文件后,我们可以用自然语言跟 AI 对话,AI 会自主决策,自主调用工具,完成 Excel 的分析任务。
原理呢,其实是只给 AI 看一部分 Excel 的局部,让 AI 了解表结构之后,调用十个工具来完成分析任务,避免 AI 直接看数据做分析带来幻觉,是我觉得做分析必须要考虑到的。

分析过程可视化:

AI 的分析过程,我都尽可能做了显式的输出,并做了前端优化让工具调用更易于阅读,目的是让 AI 做的每一步都是易于追溯的,这样可以让分析过程摆脱黑盒,让我们对分析过程掌控度更高,即使是出错了,也容易改正。

图表分析:

这里我先加入了 bar (柱状图), line (折线图), pie (饼图),scatter (散点图), radar (雷达图), funnel (漏斗图)。你可以指定 AI 输出什么图,如果不指定,AI 会自主决策输出什么图

知识库

考虑到有时候我们的 Excel 文档有很多 AI 不易理解的字段或信息,我加入了知识库功能,会在每次问答前进行召回,这样,有一些特殊需求我们就可以放在知识库里

智能联表

这个是考虑到有时候需要多表联查,但是很多朋友没有数据库基础,这里选定两张表,可以触发 AI 推荐联表的外键跟连接方式,实测,还是很准的,基本上不用自己去考虑怎么联表方式。

实测,模型能力越强,回答越精准,所以推荐佬们用 Sota 模型,站里很多公益站的模型就很不错!

大概功能就是这些,希望有建议的佬随时互动,我会认真看并改进。
以后会不断开发 AI 智能体项目,并开源给大家,希望多多支持!
GitHub 地址:GitHub - stark-456/ExcelMind: AI 智能分析 Excel 文件,对话式完成多场景 Excel 分析任务,解决 Excel 报表分析复杂、效率低等痛点


📌 转载信息
原作者:
fengling666
转载时间:
2025/12/30 10:20:29

书接上回:【Github 开源】Obsidian 插件:obsidian-github-stars-manager
小工具,加了一些小更新,不多, GitHub: 代码仓库

本来想上架官方仓库之后再发的,目前看来遥遥无期,想问点问题,熟悉 Obsidian 插件开发、上架的大佬来解惑一下

1、Obsidian 插件上架周期大概多久,PR 一直处于原地打转的状态
2、有没有什么检验代码规范的插件或者其他工具,每次 PR 都要等待,然后解决 bot 提的不规范代码,目前用的 eslint-plugin-obsidianmd(Claude 给的建议)
3、当前使用 json 存储数据,但是有同学反馈 star 数超过 600 会有卡顿现象,有没有必要换 SQLite 或者 LokiJS(AI 给的建议)

接下来准备做的:

添加标签颜色,同步选择显示颜色
标签信息可修改
删除主题切换功能,鸡肋
优化性能

看看收藏的 L 站大佬们的项目

其他

  1. 随缘更新:能用就行原则
  2. 风格参考:站内看到的各种 github stars 管理项目

感谢站内公益站点!

Anyrouter、Wong、随时跑路、Cubence、Duckcoding、我爱 996

依然暂时提供插件压缩包

github-stars-manager.zip


📌 转载信息
原作者:
sparks
转载时间:
2025/12/30 10:19:06

经常看到有人问数据库连接工具,我平时一直在用 jookdb,比较简单轻便,也可以支持单元格复制和数据迁移。
但它商业版对连接数有限制(我这边常用场景会超过 10 个连接),而开源版我也没法顺利打包跑起来,所以干脆自己在 jookdb 开源项目基础上动手做了个分支版:OpenDBKit

先声明:项目还很原始,Bug 和不完善的地方肯定不少,更像我自用的半成品,发出来主要是求建议/求拍砖。

GitHub:
https://github.com/jsnjfz/OpenDBKit

目前大概能干啥

  • 多连接管理 + 资源树(库/表/常用操作)
  • SQL 查询 + 结果展示
  • 表数据浏览/筛选/排序/复制,支持直接编辑
  • 一些基础的表结构查看/编辑(还在补齐)
  • 导出 CSV/TSV/XLSX (够用但不算完善)

我是怎么写出来的

  • 基于:jookdb 开源代码(感谢原作者)
  • 实现方式:全程是 Codex + Claude Code 的 vibe coding

现状与致歉

必须坦诚地告诉大家,目前的版本还非常原始。

因为主要是靠 AI 生成,代码风格可能不够统一,逻辑上也存在不少 bug 和待优化的地方。它现在能跑通基本的连接和查询,但离“成熟的生产力工具”还有很长的路要走。

目前的特点:

  • 基于 JookDB:继承了其清爽的界面和 Qt 的高性能。
  • 完全开源:移除了原版的构建限制,旨在打造一个自由的社区版本。
  • 轻量级:启动速度快,内存占用低(得益于 C++)。

求反馈

由于代码还比较“稚嫩”,如果大佬们在看源码时发现写法奇怪的地方,请轻喷😂,也欢迎提 PR 帮我(和 AI )修修 Bug 。

另外提醒:不建议拿它对生产库做高风险操作,重要数据先备份、能只读就只读。

感谢 🙏


📌 转载信息
原作者:
jsnjfz
转载时间:
2025/12/30 10:15:27

还在为 Gemini 生成图片右下角的半透明水印烦恼吗?无论是商务演示、设计稿制作还是社交分享,那个小小的 Logo 总显得格格不入。

现在,这款开源、免费、极速Gemini Watermark Tool 来了!


核心优势

  • 数学级无损还原:不同于传统的 “涂抹” 或 AI 补全(Inpainting),本工具采用 ** 逆向 Alpha 混合(Reverse Alpha Blending)** 算法。它通过精确计算水印透明度,还原像素原始数值,不模糊、不扭曲,画质几乎零损失。
  • 全自动识别:智能检测图片尺寸,自动匹配 48px 或 96px 水印规格,无需手动框选。
  • 极致轻便
  • 批量处理:支持成百上千张图片秒级处理,极速高效。

使用方法

  1. 极简模式:直接将图片拖拽到工具图标上,瞬间完成。
  2. 在线版本:访问 re.easynote.cc,即开即用。
  3. 开发者模式:支持命令行操作,方便集成到自动化工作流中。

技术背景

本工具由 Allen Kuo (kwyshell) 开发并开源(MIT 协议)。它通过重建水印的 Alpha 通道图层,利用公式 original=(watermarkedα×255)/(1α) 完美逆转图像叠加过程。


立即体验,告别水印瑕疵!

在线版:https://re.easynote.cc

开源地址:GitHub/allenk/GeminiWatermarkTool

温馨提示:本工具仅供个人及学习使用,请尊重原作者的内容策略及法律法规。


📌 转载信息
原作者:
whyqq
转载时间:
2025/12/30 10:13:57

背景

看过 afimory 这样的开源项目,更多像是纯粹的的照片展览了,与我的需求稍微有点不一样

Mo Gallay

mo gallary 是一个基于 nextjs​honojs 的项目,由于 honojs 的特性,可以直接部署在 vercel,专为摄影爱好者打造的照片博客平台。

类似于博客,但其更偏向于照片叙事,生活分享,个人展示方面

功能

  • 使用 postgreSQL(supabase)
  • 支持评论,精选照片
  • 支持 github,r2 存储源
  • 照片故事
  • 照片 exif 信息参数,色卡盘
  • 画廊多视图切换
  • 夜间模式切换

当前存在博客菜单,感觉和其本质有点出入,未来可能会将博客砍掉

特点 & 未来(目标)

  • 支持文件路径调整
  • 存储源迁移(如 github 迁移到 r2)
  • 照片集锦
  • 组图查看

全部 vibe coding 的项目,bug 有点多,就不便直接开源,待我再打磨打磨

预览地址:

移动端访问问题较多,建议 pc 访问

欢迎提供建议

首页



画廊




管理页面





📌 转载信息
原作者:
shaioo
转载时间:
2025/12/30 10:13:20

下载地址:

特点:

  • 批量操作 - 一键关闭、休眠或复制多个标签页
  • 标签页分组 - 自定义颜色和名称,随心折叠和展开
  • 闪电搜索 - 通过标题、网址或自定义名称实时筛选
  • 快速刷新 - 点击任意网站图标即可刷新页面
  • 自定义命名 - 重命名标签页,告别混乱的标题
  • 拖放排序 - 自由调整标签页和分组的顺序
  • 标签页休眠 - 释放内存,提升浏览器性能
  • 一键恢复 - 标签页关错了?右键菜单轻松找回
  • 明暗主题 - 适配您的浏览偏好
  • 字体大小调节 - 自定义文字大小,打造最舒适的阅读体验



📌 转载信息
转载时间:
2025/12/30 10:11:52

下载最新版本 esimplus,输入优惠码 NYGIFT2026(不是 AFF,是官方活动)就能获取一个月免费美国手机号,快去试试,随时失效,反正我领到了,好像说是只能 ios。领取后要邮件联系激活手机号,怕麻烦的可以润了,我挺怕麻烦的


为了验证是否是个例,我又注册了一个账号,又领到了,不过一个 ip 只能领取一次,这个号码可以更换的,不过是随机更换




📌 转载信息
原作者:
yonxinli
转载时间:
2025/12/30 10:09:57

别的论坛看到的,顺手转啦

泄露版:
magnet:?xt=urn:btih:4256e89216a2a9fc296ff191307206bc1790eb01

多音轨压缩版:
magnet:?xt=urn:btih:9a270c762d3248be1f6c941c916256710684aeae

HEVC 重新编码版本,只保留中文音轨和字幕 1080P 11.3G 已更新


📌 转载信息
原作者:
chinajojo
转载时间:
2025/12/30 10:08:48