feat: 新增CTC云端笔记仓库功能

- 新增多语言国际化文案支持笔记仓库模块
- 配置Universal Links与App Links支持ctc.s2ss.com域名跳转
- 实现CTS会话入口与会话时间更新逻辑
- 新增CTC笔记完整服务栈:API客户端、本地存储、同步服务
- 新增笔记编辑、预览、冲突解决、版本对比组件
- 新增二维码扫码/分享功能与路由配置
- 修复UrlAnalyzerService调用参数冗余问题
- 修复ProfileHeader组件样式问题
- 统一macOS部署目标版本为13.0
- 抑制liquid_glass_widgets高频调试日志
This commit is contained in:
Developer
2026-06-11 08:46:46 +08:00
parent 7a6d555e4c
commit 016ad3cea1
60 changed files with 8372 additions and 760 deletions

View File

@@ -1,83 +1,307 @@
<?php
/**
* 笔记仓库 API - 服务端 v2
* 创建时间: 2026-06-11
* 更新时间: 2026-06-11
* 作用: 基于Minimalist Web Notepad扩展的云端笔记API
* 上次更新: 修复钥匙校验、JSON模式POST、CORS预检、超1MB响应
*/
$base_url = getenv('MWN_BASE_URL') ?: 'https://ctc.s2ss.com';
$save_path = getenv('MWN_SAVE_PATH') ?: '_notes';
$max_note_size = 1048576; // 1MB
$rate_limit_file = getenv('MWN_RATE_FILE') ?: '_rate_limits';
$rate_limit_per_minute = 120; // 每IP每分钟120次提高限制
// CORS 头(在所有响应前设置)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 86400');
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
function save($path, $text)
{
file_put_contents($path, $text);
if (!strlen($text)) {
unlink($path);
}
return true;
// CORS 预检请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
die;
}
/**
* 钥匙格式校验
* 规则: 仅数字和字母2-64位
*/
function validate_key($key) {
return preg_match('/^[a-zA-Z0-9]{2,64}$/', $key);
}
/**
* 速率限制检查基于IP文件存储
*/
function check_rate_limit() {
global $rate_limit_file, $rate_limit_per_minute;
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$now = time();
$file = $rate_limit_file . '/' . md5($ip);
if (!is_dir($rate_limit_file)) {
@mkdir($rate_limit_file, 0777, true);
}
$data = ['count' => 0, 'window' => $now];
if (is_file($file)) {
$raw = @file_get_contents($file);
if ($raw) $data = json_decode($raw, true) ?: $data;
}
// 新窗口
if ($now - ($data['window'] ?? 0) >= 60) {
$data = ['count' => 1, 'window' => $now];
} else {
$data['count'] = ($data['count'] ?? 0) + 1;
}
@file_put_contents($file, json_encode($data));
if ($data['count'] > $rate_limit_per_minute) {
json_resp(['code' => 0, 'msg' => '请求过于频繁,请稍后再试'], 429);
}
}
/**
* 内容审核(关键词过滤)
* 返回true表示通过false表示违规
*/
function content_audit($text) {
if (empty($text)) return true;
$blocked = [
'/赌博/i', '/色情/i', '/代开发票/i', '/办证/i',
'/贷款/i', '/信用卡套现/i', '/刷单/i',
];
foreach ($blocked as $pattern) {
if (preg_match($pattern, $text)) {
return false;
}
}
return true;
}
// 执行速率限制检查
check_rate_limit();
/**
* 保存笔记内容
* 返回: 'ok' | 'size_exceeded' | 'audit_failed'
*/
function save($path, $text) {
global $max_note_size;
if (strlen($text) > $max_note_size) {
return 'size_exceeded';
}
if (!content_audit($text)) {
return 'audit_failed';
}
file_put_contents($path, $text);
if (!strlen($text)) {
@unlink($path);
}
return 'ok';
}
/**
* 返回JSON响应
*/
function json_resp($data, $code = 200) {
header('Content-Type: application/json; charset=utf-8');
http_response_code($code);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
die;
}
/**
* 获取笔记信息(大小、修改时间)
*/
function get_note_info($path, $key) {
if (!is_file($path)) {
return null;
}
$stat = stat($path);
return [
'key' => $key,
'size' => $stat['size'],
'mtime' => $stat['mtime'],
'exists' => true,
];
}
/**
* 从REQUEST_URI解析钥匙
* 支持 /key?params 和 /index.php?key?params
*/
function parse_key_from_uri() {
$uri = $_SERVER['REQUEST_URI'] ?? '';
// 去掉query string
$path = parse_url($uri, PHP_URL_PATH);
// 去掉前导/和index.php
$path = preg_replace('#^/index\.php#', '', $path);
$path = ltrim($path, '/');
return $path ?: null;
}
// ========== API 路由 ==========
// 新建随机地址笔记
if (isset($_GET['new'])) {
$path_url = substr(str_shuffle('234579abcdefghjkmnpqrstwxyz'), -5);
$path = $save_path . '/' . $path_url;
$url = $base_url . '/' . $path_url;
if (isset($_GET['text'])) {
$text = $_GET['text'];
if (save($path, $text)) {
echo("$url");
}
die;
}
if (isset($_POST['text'])) {
$text = $_POST['text'];
if (save($path, $text)) {
echo("$url");
}
die;
}
$path_url = substr(str_shuffle('234579abcdefghjkmnpqrstwxyz'), -5);
$note_path = $save_path . '/' . $path_url;
$url = $base_url . '/' . $path_url;
$text = '';
if (isset($_GET['text'])) $text = $_GET['text'];
if (isset($_POST['text'])) $text = $_POST['text'];
$result = save($note_path, $text);
if ($result === 'size_exceeded') {
json_resp(['code' => 0, 'msg' => '内容超过1MB限制'], 400);
}
if ($result === 'audit_failed') {
json_resp(['code' => 0, 'msg' => '内容包含违规信息'], 400);
}
// JSON API模式
if (isset($_GET['json'])) {
json_resp([
'code' => 1,
'msg' => 'created',
'data' => [
'key' => $path_url,
'url' => $url,
'size' => strlen($text),
]
]);
}
echo($url);
die;
}
if (!isset($_GET['note']) || !preg_match('/^[a-zA-Z0-9_-]+$/', $_GET['note']) || strlen($_GET['note']) > 64) {
header("Location: $base_url/" . substr(str_shuffle('234579abcdefghjkmnpqrstwxyz'), -5));
die;
// 检查笔记是否存在及变更(轻量接口)
if (isset($_GET['check']) && isset($_GET['keys'])) {
$keys = explode(',', $_GET['keys']);
$results = [];
foreach ($keys as $key) {
$key = trim($key);
if (!validate_key($key)) continue;
$note_path = $save_path . '/' . $key;
$info = get_note_info($note_path, $key);
$results[$key] = $info;
}
json_resp(['code' => 1, 'data' => $results]);
}
$path = $save_path . '/' . $_GET['note'];
if (isset($_POST['text'])) {
$text = $_POST['text'];
if (save($path, $text)) {
echo("saved");
}
die;
// 获取笔记信息(单个)
if (isset($_GET['info']) && isset($_GET['note'])) {
$note = $_GET['note'];
if (!validate_key($note)) {
json_resp(['code' => 0, 'msg' => '无效的钥匙格式'], 400);
}
$note_path = $save_path . '/' . $note;
$info = get_note_info($note_path, $note);
if (!$info) {
json_resp(['code' => 0, 'msg' => '笔记不存在'], 404);
}
json_resp(['code' => 1, 'data' => $info]);
}
if (isset($_GET['text'])) {
$text = $_GET['text'];
if (save($path, $text)) {
echo("saved");
}
die;
// 删除笔记
if (isset($_GET['delete']) && isset($_GET['note'])) {
$note = $_GET['note'];
if (!validate_key($note)) {
json_resp(['code' => 0, 'msg' => '无效的钥匙格式'], 400);
}
$note_path = $save_path . '/' . $note;
if (is_file($note_path)) {
@unlink($note_path);
json_resp(['code' => 1, 'msg' => 'deleted']);
}
json_resp(['code' => 0, 'msg' => '笔记不存在'], 404);
}
// ========== 笔记路由通过URI路径解析key ==========
// 从URI解析钥匙
$route_key = parse_key_from_uri();
// 如果没有有效钥匙,重定向到随机笔记
if (!$route_key || !validate_key($route_key)) {
// 无效钥匙格式 → 如果是API请求返回400否则重定向
$is_api = isset($_GET['json']) || isset($_GET['raw']) || isset($_GET['info']);
if ($is_api) {
json_resp(['code' => 0, 'msg' => '无效的钥匙格式'], 400);
}
header("Location: $base_url/" . substr(str_shuffle('234579abcdefghjkmnpqrstwxyz'), -5));
die;
}
$note_path = $save_path . '/' . $route_key;
// 写入/修改笔记
if (isset($_POST['text']) || isset($_GET['text'])) {
$text = isset($_POST['text']) ? $_POST['text'] : $_GET['text'];
$result = save($note_path, $text);
if ($result === 'size_exceeded') {
json_resp(['code' => 0, 'msg' => '内容超过1MB限制'], 400);
}
if ($result === 'audit_failed') {
json_resp(['code' => 0, 'msg' => '内容包含违规信息'], 400);
}
// JSON API模式
if (isset($_GET['json'])) {
$info = get_note_info($note_path, $route_key);
json_resp([
'code' => 1,
'msg' => 'saved',
'data' => $info
]);
}
echo("saved");
die;
}
// 获取笔记原始内容
if (isset($_GET['raw'])) {
if (is_file($path)) {
echo(file_get_contents($path));
} else {
header('HTTP/1.0 404 Not Found');
}
die;
if (is_file($note_path)) {
header('Content-Type: text/plain; charset=utf-8');
echo(file_get_contents($note_path));
} else {
http_response_code(404);
echo 'Not found';
}
die;
}
// ========== 默认渲染Web编辑页面 ==========
?><!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Minimalist Web Notepad ">
<title><?php print $_GET['note'];
?></title>
<title><?php print htmlspecialchars($route_key, ENT_QUOTES, 'UTF-8'); ?></title>
<link rel="shortcut icon" href="<?php print $base_url; ?>/favicon.ico">
<link rel="stylesheet" href="<?php print $base_url; ?>/styles.css">
</head>
<body>
<div class="container">
<textarea id="content"><?php
if (is_file($path)) {
print htmlspecialchars(file_get_contents($path), ENT_QUOTES, 'UTF-8');
}
?></textarea>
if (is_file($note_path)) {
print htmlspecialchars(file_get_contents($note_path), ENT_QUOTES, 'UTF-8');
}
?></textarea>
</div>
<pre id="printable"></pre>
<script src="<?php print $base_url; ?>/script.js"></script>
</body>
</html>
</html>