【转载】最强分析 |一文理解Lumen及全局光照的实现机制

最强分析 |一文理解Lumen及全局光照的实现机制

Author: [GAMES104官方账号]

Link: [https://zhuanlan.zhihu.com/p/643337359]

Dynamic Global Illumination andLumen

我们今天的课程主题是动态全局光照和Lumen ,在讲Lumen之前,我们需要首先需要了解GI(Global Illumination),如果不把GI讲清楚,大家将会陷入Lumen复杂深邃的技术细节中。
如果不了解GI的发展演变,大家会感觉Lumen中的某些细节处理是凭空从天上掉下来的。但其实不然,Lumen中的很多算法和思想在之前的一些GI算法中都有出现和实践过,因此Lumen更像一个各种GI算法的集大成者。我们只有理解GI最宏观的体系结构,你才能理解Lumen 是怎么产生的。

一. Global Illumination

第一步我们是要了解渲染中最著名的Render Equation ,这在之前的课程中也讲到过。我们今天几乎做的所有Rendering 相关的工作都是去满足这个Render Equation 。但是至今为止,没有任何一款游戏引擎能够做到实时的完全按照Render Equation来进行渲染,我们能做到的只是在无限的逼近它。

如果只计算点光(Point Lighting)的直接光照(Direct Lighting)还是比较简单的。但 GI 并不只计算点光,而是需要计算四面八方照的光,这些光和物体表面的 BRDF方程都要进行积分,才能形成我最终的光照。

这里面复杂的积分过程非常麻烦,电脑屏幕上拥有百万甚至千万级别的 pixel 。对于每个pixel,都要去采集四面八方射过来的光线,从而计算出它的积分值。离线情况尚且困难,更何况是实时的情况下,一秒钟还要渲染几十帧甚至上百帧,这里面的计算量简直恐怖如斯。

更糟糕的是,需要计算的光线数量严格意义上是无限多的。就算你在场景里面只摆放了一个光源,但这个光源可以把周围物体无数个小面片照亮,每一个被照亮的小面片依旧可以视为一个新的光源来照亮其他物体。同时被这些小面片照亮的其他地方也会继续反弹光线。我们把只进行一次反弹叫做 SingleBounce,把多次反弹叫做 MultiBounce 。
上图右侧是著名的渲染场景康奈尔盒(Cornell Box),通过比较渲染场景和同一场景的实际照片来确定渲染的准确性。它的左边是红色,右边是绿色,上面放置一个光源,下面放有一个长方块和一个方形块。这里需要注意的是,上方放置的光源并不是点光源,而是一个面光源,因此光源本身就可以分解成无限多个。就算是Cornell Box 这么简单的场景,如果你想把它做到接近真实也是非常困难的,并且如果想要做到实时渲染更是难上加难。
我们如果仔细观察,这个简单场景中的的光学现象其实非常复杂。光源照射到红色墙反弹回来后会出现很多红光,红光会照亮这长方块的左侧面。同样,右边墙上也看到绿色。而这就是 GI 的效果。


我们以游戏为例,如果游戏中只有直接光照,最终的效果就如上图左侧一样。而通常我们在游戏看到的效果并非如此,因为通常我们还会加上环境光。环境光其实是对 GI 的一个 hack,如上图右侧。虽然有了环境光的效果会让场景显得好了一点,但依旧达不到真正GI非常丰富且复杂效果。
阳光投进洞口的岩洞,拉开窗帘的房间,打着手电的傍晚,这些场景如果没有GI 来模拟复杂MultiBounce 的光学现象,整个画面会看上去非常塑料。因此我们需要去实现GI的这种效果。
Monte Carlo Integration


如之前所述,Global Illumination 中最复杂的问题就是积分,通常来讲,我们可以使用蒙特卡罗(Monte Carlo)积分法来解决这个积分。蒙特卡洛是一类统计模拟的积分方法,在求解积分时,如果找不到被积函数的原函数,那么利用经典积分方法是得不到积分结果的,但是蒙特卡洛积分方法告诉我们,利用一个随机变量对被积函数进行采样(Sampling),并将采样值进行一定的处理,那么当拥有一定数量的采样时,最终估算得到的结果可以很好的近似原积分的结果。这样一来,我们就不用去求原函数的形式,就能求得积分的近似结果。
他的想法其实非常简单,假设对于一个函数f(x)区间 A 到 B ,我们在做Render 时的积分区间就是在法线朝向的半球上进行积分。

我们对于区间A到B内的点进行采样,得到每一个点的函数值,然后进行平均,就能知道你的这个积分的估算值是多少。对于 Shading 来讲,就是采样多个方向的光线计算的Radiance,最后把他们加在一起。

二.Monte Carlo Ray Tracing

使用Monte Carlo 求解GI 最直接的应用就是Monte Carlo Ray Tracing。
Monte Carlo Ray Tracing的思想非常简单,首先从我们的眼睛出发,也就是屏幕上的每个像素,发出射线(Ray),这些射线会射中场景中的某个物体。我们从被射中的位置点出发,再往它的四面八方去投射射线(1 Bounce),这些发出的射线会打中其他的物体,它又会在新的点向四周发射射线(2 Bounce),以此类推。在此期间,如果某根Ray射中了光源,那么我们就可以根据这条路径计算出对应的Radiance 。
Monte Carlo Ray Tracing通过多次反弹,每增加一层Bounce的,它的计算复杂度会进行指数级的扩张。SingleBounce如果还能勉强接受,但是MultiBounce计算量会随着迭代的次数急速膨胀。
我们需要注意的是,在我们实际应用的场景中,真正对于结果有贡献,或者说有亮度的地方是非常不均匀的。比如说一个硕大无比的房间,它真正亮的地方可能就是阳光射进窗子里的那一部分。你的整个房间都是被就是被它的MultiBounce照亮。这个时候你在进行采样的时候,你会经常发现,只有极少数的射线有机会打中窗子,绝大多数射线只会采样到了房间内黑暗的部分,根本没采到光。
因此Monte Carlo Ray Tracing 最大的问题实际上就是采样,因为采样是随机的,所以没有办法保证相邻的 pixel A 和 pixel B 能够产生同样的结果,A可能采样到了光,而B则没有,因此你会在屏幕上看到很多的噪点。

如上图所示,最左上角的图中是每次只发射一根射线,可以看到会有很明显的 Noise 。之后它每一张图从左到右从上到下采样数量都是翻倍的。
因此右下角图中的射线数量是2的16次方64,000 条射线。可以看到最后渲染的结果已经非常 Smooth 了。如果我们只需要一次Bounce 的结果 ,如果硬件够狠,几分钟你也许能渲染出来,但是如果是 MultiBounce 的话,依旧需要非常非常久的时间才能得到结果。
综上所述,你会发现基于Monte Carlo的方法,你的 Sampling数量越多效果越好,但是也会越慢。如何 Sampling是所有基于蒙特卡罗的GI 方法的核心。

Sampling最简单的方法就是均匀采样 (Uniform Sampling)。但是绝大部分情况下信号的分布是不均匀的。使用Uniform Sampling除非密度很高,否则的话得出的结果其实并不尽如人意。如同上面我们的例子中,在一个房间内只有一个窗子是亮的,渲染的时候你在半球内实施Uniform Sampling,只有很少的射线能够采样到窗户。这就是Uniform Sampling 的一个问题。

我们要引入一个很有意思的概念叫概率密度函数(probability distribution function )PDF。我们按照PDF的概率去选取我们的采样点,而不是按区间A到B均匀分布。我们只要在积分中除以选取采样点的概率,最后算出的积分是等价的。

如上图所示,蓝色函数是我们要求解的函数,我们选取与被积函数 f(x) 相似的概率密度函数 p(x) 。根据 p(x) 在f(x) 越大的地方,采样点选取的概率越大,也就是信号强烈的地方,我们的采样点会比其他地方更多,在f(x)值较小的地方,采样点的选取的概率越小。在数学上可以证明,这样的采样方法,可以用尽可能少的采样来获取更为逼近的真实积分值的结果。
这个概念其实也很简单,我们尽可能朝着光比较亮的地方,或者尽可能沿着我的法线正对的地方去多投射一些采样的射线,那这样的话我就可以用更少量的射线来获得我想要的结果。

我们在做 Rendering 的时候,如果假设大部分反射是 diffuse的 ,对光的这个敏感度就是一个Cosine Lobe,靠近法线(Normal)的方向会敏感,然后对其他的角度衰减扩散。就算你的光线很强,但你是从很侧面射来,我对这个光的感应度其实也并不高。

如上图,左边是Uniform Sampling,而右边是Cosine Lobe,也就是采样点的分布会稍微密集的靠近天顶的地方,你可以发现同样是256 spp( Sampling per pixel),使用Uniform Sampling会有很多噪点,而Cosine Lobe噪点会明显下降 。

如上图所示,GGX材质高频足够高低频足够宽。这种情况下,我们使用GGX 的PDF会比使用Cosine Lobe更为Sharp,更能表达Glossy的效果。
总而言之,GI 核心是你需要去找到一个好的 Sampling 方法,用尽可能少的射线去获取一个来自于四面八方的光场提供给你的信息。

三.Reflective Shadow Map(RSM )

前面我讲了Monte Carlo Ray Tracing的GI 算法,但它是一种离线方案,通常是在电影行业中使用,因为这种方案对于实时计算的游戏来说太过昂贵。这一章节们来讲 Reflective Shadow map(RSM ),它核心解决的问题是我怎么把光注入到场景中去。
在图形学里面,我们大致有两类的GI方法,一类就是之前最经典的Monte Carlo Ray Tracing。另一个著名的方法叫做光子映射(Photon Mapping)。
Photon Mapping 的思想非常有意思,和我们之前的Ray tracing从相机为原点发出射线向整个世界不同,Photon Mapping 是从光源出发。Photon Mapping的思想是:我们之所以能看到世界中的物体,原因是因为从光源射出来的光子打到了物体表面,进而不停地反弹,人眼收集到了这些光子,进而你看到了整个世界。因此Photon Mapping使用光学中最基础的概念,从光的角度去看待整个问题。
关于Photon Mapping具体的实现细节,我们今天就不展开了。简单的讲Photon Mapping就是说我发射出无数的光子,然后这些光子在物体的表面来回 Bounce 。当到达一定条件后,那些光子就会停留在物体表面。我们在做 Shading 时,收集这些Photon然后进行插值,最后得出Shading的结果。 这就是Photon Mapping一个核心思想,大家如果有兴趣的话可以查阅相关的资料。

接着我们来讲 RSM ,它的想法基于一个直观的观察。我们在渲染阴影的时候,需要渲染一帧 Shadow Map,Shadow Map的渲染和正常的渲染不同,正常的渲染是从当前相机的视角去渲染。而 Shadow Map 是从光源的位置和视角去渲染。
因此,如果我们从光源的位置和视角,把整个场景正常渲染一遍,当只有直接光照的时候,所有被光源照亮的物体表面都会渲染到我的Map 里面。换句话说,从光的位置,你能看到所有被它直接照亮的表面。所有被第一次被灯光照亮的表面将被我渲染进去,一个像素不会多,一个像素也不会少。
当然这里我们除了存储光照通量之外,还需要存储WorldPos 和Normal,以提供给后续 Shading 读取。那么在做Shading 的时候,就提前拥有了RSM,我们就能知道所有在空间中被光源第一次照亮的点和它对应的亮度信息。
对应上图,假设我们要渲染X点的光照值,X点因为被桌子挡住,光源无法直接照亮这个点,因此X点并没有直接光照。我们根据之前渲染的那张Reflective Shadow Map,得知光源会射到 Xp 点,Xp点会沿着它的法线方向进行散射,我就可以把Xp 点的 Radiance 给接过来进行 Shading 。这就是RSM的一个核心想法。

以此类推,最简单直接的方法就是将RSM中的每个像素都当成是一个小光源,把RSM上的每一个点都进行一次渲染,但这个方法有个问题就是过于粗暴,比如RSM分辨率大小是 512x512,那也是将近有几十万个点。把每个像素都当作光源渲染一遍是一个极度缓慢的过程。

我们的改进的思路也很简单,距离我们比较近的间接光照对我们的影响最大,比较远的那些采样点贡献度则更低,因此我们不去对RSM上每个点都去采样,而是在世界空间中去不同方向发射Cone Tracing ,并且各个方向的方向采样密度不相同。如果某个方向上采样点距离比较远,我们采样的密度低一些,如果比较近,我们采样的密度会高一些。

RSM只需要 400 次采样就能得到 1 bounce 的 GI 的效果,这也说明间接光照其实是非常低频的数据,它并不像直接光照那么高频。根据这个思想,我们可以用更低分辨率来计算间接光照,也就是每隔两个或者每隔四个pixel来做一次间接光计算,旁边像素可以跟共用周围像素计算的结果。
当然这也会产生一些问题,如果采样点和我当前渲染像素空间位置相差很大,或者法线朝向不共面,会产生很多Artifact 。当然这些渲染错误的 pixel 不会很多,对于整个屏幕来讲可能不到 1% 甚至千分之一。因此对于这种情况,我们就认为这是个无效的差值,重新对它进行一次完整的采样。

很早以前的游戏中其实已经实现了RMS,所以你可以在游戏中看到手电筒照照亮周围物体的效果。

我们非常感谢RSM,首先想到了用把光子注入到世界当中这种方法。并且给我们提供了可以在更低分辨率的屏幕空间里面去采集这些间接光照的思路,还给我们提供了出现了 Error时,处理这些错误像素的方法。Lumen中很多地方都使用了这种思路。
RSM毕竟是个早期算法,有很多的局限性。首先是只能解决 SingleBounce ,而且它也不检测 Visibility ,并且没有考虑间接光照的遮挡。但是毫无疑问RMS 是一个非常具有启发性的一个工作。

四.Light Propagation Volumes(LPV)

既然光子已经注进去了,那我们就要让光在这个世界里面流动起来。这里就要介绍一个也非常具有启发性的算法 Light Propagation Volumes(LPV)。

LPV最早是 Cry Engine 3 提出的一种实时、无需任何预计算的全局光照技术。

在RSM中,我们找到并定义了一系列的虚拟点光源,LPV的第一步仍是如此。它在找到虚拟点光源后,把整个场景分成的一个个小格子叫做Voxel(体素)。

我们需要把在RSM得到的虚拟点光源“注入”到它对应的Voxel中。
由于在Voxel里可能有多个虚拟光源存在,因此存储时可以使用多个Cubemap来暴力存储,但是这会造成巨大的存储开销,通常来讲我们可以用二阶的球谐函数(Spherical Harmonics)SH来拟合这里的光照结果,因为球谐函数SH能够将光照是加权累积在一起。最后我就得到了在小格子里面Radiance 在空间上的分布场。
对于任何一个物体,在Shading 的时候,就可以拿到世界空间中每个Voxel内部光照的 SH 进行光照计算。

当我们有了Voxel 之后,还需要对场景进行扩散。对于一个Voxel,在传播时可以传播上下左右前后共六个格子。它从每个Voxel发出对应方向的射线,例如穿过了Voxel的右面就传播到右边的相邻格子。

这种扩散叫Propagation,LPV的主要问题是当Radiance 扩散出去之后,你这个Voxel内部还有没有 Radiance ,如果内部还有Radiance ,那么能量是不守恒的;如果 Radiance 不在,那个地方就是黑的,那么周围为什么还能被照亮呢?
我们会发现这个光的扩散的速度扩散的范围跟你做了多少次 Iteration 有关,这种感觉就像是说光是有限速度在各个这个 Radiance 里面去传递的。因此LPV 的方法不太符合物理学原理。
为什么要提起这个算法呢,我觉得最核心的就是他是第一个把空间进行 Voxel 的划分,在每个Voxel 里面去保存Radiance 的分布情况,并且更有意义的是他提出了把Radiance 的分布用SH存储的方案。

五. Sparse Voxel Octree for Real-time Global Illumination (SVOGI)

接下来我们要介绍 SVOGI ,SVOGI是对之前LPV的空间的升级,之前的LPV虽然对空间进行Voxel 划分,但每个Voxel如果你分得太粗,就不能够准确地表达这个世界里面的光的分布,如果分的太细,那我就将拥有极其多的Voxel。并且物体里面的空间是是中空的,没有Voxel存在的必要,因为Voxel只需要存物体里面的光照信息。

因此SVOGI提出一个叫做保守光栅化的方法。一般来讲,决定一个三角形是否占有一个像素的标准是三角形是否覆盖了像素的中心,保守光栅化则是只要覆盖到像素的任何区域我都会进行Voxel。比如很小很薄的三角形只占有像素的很小部分,依旧会进行Voxel。

如上图所示,我们把表面的 Voxel 全部收集起来。

如果我们要表达的空间非常大,自然而然我们可以使用树来存储。大家知道空间上我的每一个维度上都进行二分,三维空间上八次分,因此我们使用八叉树。如果这个区间里没有对应物体,这颗树就会分得特别粗,如果有物体则会继续细分。
实际原文的实现非常复杂的。对于每一个节点,它不仅存自己的数据,还要存了周围的数据。因为它要做 Filtering 的时候需要这些数据来做差值,因此所需要的数据结构惊人的复杂。
我们很努力的想找到它的源代码,但很遗憾由于我们的信息渠道不够充沛,并没有找到源代码。所以有一些他们没有讲到但我们很关注的一些细节无法得知。
但他首先意识到Voxel的空间分布是不均匀的,基本上只要用到空间结构表达,使用这种Octree Tree空间划分的思想就是一个很自然而然的想法。

SVOGI 另一个很启发性的思路是,通常采样时你需要采样几百根Ray才能达到你想要的效果。他的想法是我想要重点采样的地方,并不是使用Ray而是Cone,Cone的英文是圆锥,当圆锥体展开的时候,对应的面积就会越来越大,那它在远处就会扫到更大颗粒的Voxel。由于Voxel 对空间的表达是一个树状结构,越靠近树的根部,它Voxel表达的空间区域就越大。所以你远处的颗粒更大的Voxel存储的值,实际上是一个很大面积的Radiance 的集合。
SVOGI提出的这个思想非常的好,因为你只要对空间进行结构性的Voxel 表达,你就会有这样的优势。

六.Voxelization Based Global Illumination (VXGI)

SVOGI的很多细节我们查不到,但是这并不重要,因为实际上没有人再用这个算法。原因很简单,八叉树的数据结构在 GPU 上表达非常的复杂。这在VXGI发明后,一切都被变成了浮云。
VXGI的思路是,我们其实并不需要整个场景的表达,对于 GI 来讲,我们最重要的工作是把我眼睛看到的地方的GI做好,并且离我比较近的区域的GI更为重要,远处区域的 GI虽然也重要,但是我不需要对它进行很高精度的采样。
这个时候我们使用传统的Clip Map 的思想,离我相机近的地方,我用更密的 Voxel 去表达;离我远一点精度就下降一倍,以此类推。
这样就不用极其费劲的去构建一个稀疏的八叉树结构,我依旧还是构建了一个树状结构,并且这个树状结构基于View的密度分布,对 GPU 更加友好,并且实现起来更加的清晰明确。

由于场景中你的相机总是在动,那么对应的ClipMap 也要跟随着进行更新。VXGI提出一种可循环的寻址系统。当你的相机移动变化时,我们只需要进行增量更新,也就是在 GPU 中,你每次只需要更新它边上那圈数据,而不需要把中间那些没有必要更新的数据重新生成和赋值。

这样我们对整个空间就有一个近处密、远处稀疏的Voxel 表达。如上图所示,远处的 Voxel 也没有显得特别稀疏,这是因为随让远处的Voxel很大,但是因为透视投影的原因,在屏幕空间中感觉并没有没有特别明显,但实际上它已经离你很远了。

VXGI 还需要考虑透明度的问题。每个Voxel 并不是只有透光和不透光两个状态。实际每个 Voxel 有一个 Opacity 。假设Voxel里面有一个Mesh,这个Mesh把55%的光挡住,而45%的光透过,并且各个方向的透光率不一样,因此我们要沿着它各个方向算出它的不透明度。
我们在采样时,不像大家想的那样 hit 到了一个 Voxel 我就停在那里,而是有一个有半透明效果。

如上图所示,对于一个场景而言里面的Voxel 会有很多半透(灰色)的区域。

对于VXGI,我们同样使用RSM,注入到我们每一个表面的Voxel 上去。

大家可以看到只有一些 Voxel被照亮 ,因为只有这些Voxel 接受到了直接光照。

接下来,我们对于每个屏幕上的像素,进行Cone Tracing 。对于非常粗糙的表面,我就以Cosine Lobe四面八方的采样;如果表面是比较光滑我就沿着大致的反射方向采样;如果非常光滑的表面,我不仅只沿着反射方向去取那个间接光照,Cone还会变得非常的细。

根据之前所讲,如果沿着这个方向的 Opacity不是完全遮蔽,那么光能透露过来。这时候我再继续往后走,越往上走,我的采样面积就越大。我就取 Clip map 里面更高的 Mip 的那个Voxel,这样我一次性就能覆盖很大的区域。
我光线的透过的Alpha会随着我不断的透射而变低,当Alpha小于某个阈值,很接近于零的时候,我们就说,就当做不透明算了,我们就不往后再走了,这样能让我们的算法更快一点。

VXGI 实际上是我个人认为非常好的一个算法。虚幻引擎中有一代里面有一个插件就可以实现 VXGI。我对所有能够实装的算法都是比较敬畏的。
因为你真的敢把一个算法放出来,让大家在各种场景里测试的话,说明它本身的效率性能和它的鲁棒性已经达到了一定的程度。

VXGI 其实也有很多的问题了,比如实际的Cone Tracing中opacity 累积是一个估计值。
如上图中右上所示,绿色三角形在Voxel 里面阻挡了一部分光照,黄色的方块也阻挡了部分光。它两个Alpha如果按照乘法累积肯定不为零。但是在实际上光路上其实已经完全阻挡了光照。
实际的渲染中的表现就是 Light Leaking漏光,特别是对于那种一些很薄的物体,或者本身不是很薄但是距离比较远的物体尤其严重。

七.Screen Space Global Illumination (SSGI)

SSGI的工作最早是2015年寒霜在SIGGRAPH 上发表的工作。

SSGI的原始思想很简单,如上图所示,红色框的是我直接渲染出来的东西。下面白色的部分,如果我要我去渲染它的间接光,因为他的表面非常的 Mirror ,所以我只要在屏幕空间里面把红色部分反一下,这些数据我就可以用了。因此SSGI的思路是,我在屏幕空间中把这些渲染好的像素点作为我全局光照的小光源。

如上图,假设整个屏幕中的像素已经渲染好了,当我要Shading图中黄色的点时,我已经知道了Normal和相机方向,我就可以沿着反射方向射一些Ray。这些Ray就在我的屏幕空间里面去找空间上位置正确的点,如果这个点是红色我就获得了一些红色的Radiance ,如果这个点是绿色我就收到了一些绿色的 Radiance ,这样的话我就可以直接用屏幕空间的数据进行 Indirect Lighting 。

我们在屏幕空间中找到对应采样点的方法叫做Raymarching。假设我们从一个点出发射出一根Ray,这根Ray使用均匀间隔一直往前走,如果它当前位置的深度值比我这跟Ray的深度更靠前,就说明这跟Ray被挡住了,这个时候我就认为找到了一个交点。这就是均匀的 Remarching ,它要求步进间距非常的密,速度也就会很慢。

当然我们需要另一种方法加速Remarching 。GPU 硬件提供了生成一个特殊Buffer的方法,当然我们也可以自己实现,叫做HZB,也叫做Hi-Z。它可以把 Z buffer 做成一层层的 Mip,每个上层 Mip里面的一个像素,都对应它下层Mip里面四个点的像素。上层 Mip像素点的值,是下层Mip四个像素点中深度的最小值,也就是离我最近的那个值。
如果我们拥有了Hi-Z,一根Ray如果跟 Hi-Z的某个Mip不相交,那我跟你下层的Mip一定不相交。如果我跟你某个Mip相交,那我一定跟你下层某个Mip的像素相交。所以它实际上把这个Depth Buffer 做成了一个 Hierarchy 的结构。

当我有了Hi-Z,想做Raymarching的时候 就比较简单了。比如初始状态下,我在Mip0层,我先在Mip0层往外走一格。

如果发现在Mip0层没有交点,我需要接着走,不过这时我往上跑一层跑到Mip 1走一格,这就相当于走了两格。

这个时候如果我发现还没有交点,我就开始胆子更大了,我在再往上走一层在Mip2走一格,我再做一次深度测试的话,这一次我相当于做到了四格。

这个时候假设我不小心发现我检测到了物体。如今我是在 Mip2级别,我就知道他可能交到了Mip1 或者Mip0 的某个点,但是具体是哪个点我不知道。这时候,我回退到 Mip 1往前走一格。

我发现好像跟 Mip 1也有个交点。那我再回退到 Mip 0 继续走。

这时我们就能找到交点。虽然听上去Hi-Z比均匀采样更为复杂,但是它的算法复杂度是 log 2 的,也就是说你就算是 1024 或者更高的分辨率,我也最多只需要十几步就走到了。如果你用 Uniform 的 Raymarching 可能要走五六十步才能走到。

SSGI 还有一个很有意思的思想就是复用采样。当我对一个像素求出了采样点,我周围的点在也要进行球面采样的时候,如果不考虑 visibility ,他采样到的那个小灯泡实际上也是你的小灯泡,相当于帮你也做了一次采样。这个思想其实也是非常重要的。因为在我们真正做 GI 的时候,你不可能对每一个采样点的位置射那么多的Ray,不然整个计算就会爆炸。

和之前一样,我们提供采样的Scene Color 也会做Mipmap,对于远处的Cone Tracing 会采样到分辨率更低的贴图,这相当于对整个光照进行了一次 Filtering。

当然SSGI也会有一些问题,如果在屏幕空间没有的东西,我就看不到。比如像上面例子中框出的部分因为反射的数据拿不到,所以下面的整个都变白了。但是这不影响SSGI作为一个非常有用的算法。

SSGI 拥有很多优点:

  1. 因为Hi-Z的精度非常高,所以两个物体的交接面,非常细腻的几个 pixel 的内容误差,它都能把你算出来,因此SSGI能够处理非常近的Contact Shadow。而以前 Voxel的方法对它的处理很糟糕。
  2. 对于Hit的计算,因为用了Hi-Z 的方法非常准确,这比Voxel去估计要准得多。
  3. 无论场景有多复杂,SSGI对场景的 complexity 是无感的
  4. SSGI能够处理动态的物体。

这几个优势非常重要,这就是为什么在这么复杂的 Lumen 架构里面,还使用到了SSGI 。

Lumen

首先我们需要知道Lumen需要解决哪些问题。

很多人都会问,既然已经有了硬件的Raytracing ,我们为什么还要Lumen呢。这是由于很多硬件并不支持 Realtime Raytracing,对于支持的那些硬件, N 卡还算是勉强可以,而 A 卡支持的比较糟糕。并且实际测试中,3080大概 1 秒钟能做10 个billion 的计算,这个实际的计算量用来做GI 的话,实际还是比较费力的。
比如游戏里最常见的Indoor场景,你需要每像素500 根Ray才能得到想要的效果,但是我们只能付得起每像素二分之一个Ray。因此我们需要解决如何在软件层面解决掉快速Ray Tracing 的问题。


另一个大的问题就是Sampling。之前的章节我们也讲到了Sampling,过去几十年时间离线GI都在和 Important Sampling 做殊死搏斗,直到现在也没有让我们特别满意。而我们的Realtime 的 Sampling 数量又被卡的很死,这进一步增加了我们处理问题的难度。
如上图所示,靠近窗口的Sampling 结果基本是可以接受的,虽然它有些Noisy,但是你可以通过 Filtering 解决这个问题。但是如果离窗子稍微远一点,那 Filtering 也救不了你,最终你看到的就是一个一个的大色斑。因此Lumen也需要解决如何Sampling的问题。


我们知道Indirect Lighting 可以在 Low Resolution 上面采样得出,Lumen的想法是在屏幕空间放一大堆的 Probes,屏幕空间 的特点是紧贴那些要被渲染的物体表面,通过 Probe获取光照。比如每16 个 pixel 我放置一个Probe,每个像素去 Shading 的时候,它的高频信息可以通过表面法线产生,最后得到右边非常逼真的效果。
因此Lumen最核心的思想是三点:

  1. 在不用硬件Ray Trace的前提下,我如何进行快速的Ray Trace。
  2. 尽可能的在 sample 里做的优化。
  3. 放置的Probe 尽可能贴着真实物体表面,使它的精度足够的高。

Phase 1 : Fast Ray Trace in Any Hardware

Lumen最核心的点就是要解决我怎么样的在任意硬件上能进行非常快速的 Raytracing ,它要解决如何我在射出一根Ray时,快速得到这个Ray到底能不能交到一个物体,并且知道和它相交的物体是谁。
这个 Raytracing 当然是可以用硬件Raytracing 去做,Lumen也提供能够开启硬件Raytracing 的功能来做更高精度Raytracing的表现,不过为了兼顾各种硬件,Lumen 最主要的是提供了基于软件的SDF(Signed Distance Field )的Tracing算法。


SDF叫做空间距离场,假设空间中有一个Mesh,我们会生成一个空间场的数据信息,对于空间上的任何一个点P,你都可以查询到P距离Mesh 上最近的距离是多少。如果P点在Mesh外部,那它的数值是正的;当P点在物体表面,那它的数值是0;如果P点在Mesh内部,那它就是负值。
在过去我们对一个形状的表达是使用 Triangle (点线面),它非常符合我们人的直觉。但是它是离散的,顶点数据之间并没有任何联系,它必须要通过 index buffer 关联起来。三角形和三角形之间的连接也不存在,因为它必须要通过顶点,查询到共用两个相同 index 的顶点,我才知道三角形两个边是连接起来的。
SDF从数学上是 Triangle的等价变换。他是连续和均匀的,是可微的,既然是可微的,就能做很多事情。

我们现在已经知道了SDF的定义,接下来我们来看如何生成SDF。最直接方案是,我对每个Mesh都生成自己的SDF。对于一个游戏场景来说,如果有上万个物体,实际的场景物体可能就是几百种物体通过平移旋转放缩得到。我只需要存这几百个物体的 SDF加上它的Transform ,我就可以把场景表达出来。

在生成SDF的时候,需要考虑Mesh特别细小于我距离场密度的情况。如上图左侧所示,如果我们采样到的是5和5中间红色点的位置,因为插值的关系,我们采样到的还是5。因此我们在这里把Mesh撑开一点。因为生成的时候会做这样的操作,因此我在做Trace 的时候也会进行一个偏移的修正。

接下来我们要解决如何使用SDF 进行Ray Tracing 找到交点。我们这里使用Raymarching的方法,不过我们这里是在世界空间中去做,而不是之前在屏幕空间。Raymarching的主要问题就是步长如何选择。
如右图所示,从出发点P0出发,你的第一个Distance 距离就是P0点SDF的值,因为SDF是你距离最近的Mesh的距离,小于这个值的范围内根本不会有任何物体。这时你就会从 P0 点跳到 P1 点,从 P1 点再去找它的SDF,可以找到 P2 点,这样以此类推,我们就可以非常快的hit 到物体的表面。
实际上这是比较安全的,就算你穿进物体内部了,因为 SDF 它是有符号的,进去之后它会给你一个负数,那个负数就告诉你说你的表面应该在哪,你还可以弹回来。所以使用SDF 的Raymarching既快又鲁棒,这是 SDF非常大的好处。


SDF第二个好处是做Cone Tracing 。比如说我们要做 Soft shadow ,从需要计算阴影强度的着色点o出发,向光源发射一条Shadow Ray,沿着该方向进行marching ,每marching 到一个点,我们可以得到一个圆心为当前点,半径为当前点的SDF值的球,从出发点向该圆作一条切线,可以得到一个夹角,取所有这些夹角中的最小值,根据这个值来确定半影大小。


由于SDF的显存占用过高,我们其实可以对SDF 进行稀疏化处理,使用间接存储的结构。如上图所示,我们可以把那些 Voxel的 distance 大于某个阈值(体外)和小于某个值(体内)的Voxel全部干掉,用一个简单的 index 就可以把SDF的存储减少很多。

但是我个人一直在怀疑一件事情,确实只要大于一个阈值,你把那些 Voxel 全部标为空,你这样只要存那些有用的区域,但它会导致你marching 迭代的步长变长。因为以前一次性可以跳一大步,但现在如果是空的话,我只能一步一步的试,但是这个里面工程上肯定有办法可以解决,我们就不展开了。


SDF 还可以做LOD,而LOD 还有一个很有意思的属性,因为它是空间上连续的可导的,你可以用它反向求梯度,实际就是它的法线。也就是说我们用了一个 Uniform 的表达,可以表达出一个无限精度的一个 Mesh ,我既能得到它的面积,又能对它进行快速的求交运算,还能够迅速的求出它连续的这个法线方向。

如果你使用 LOD 和Sparse Mesh,可以节省40%到 60% 的空间,这在硬件上还是蛮可观的。对于远处的物体,可以先用这个 Low level 的SDF,当你切换到近处的时候再去使用密度高的SDF,这样也可以很好的控制内存消耗。


如果你用Per-Mesh SDF去做Raytracing,对于单个 mesh 来讲,你做 Raytracing速度会很快。但是你架不住场景的物体数量特别的多,比如说我一根Ray打过去的话,沿途所有的 Object 我都得问一遍。如果场景全是物体,那你的计算复杂度就会越来越高。如上图所示,越亮代表Step的次数越多,会发现越靠近 Mesh 的边界上的像素,它需要的Step 就越多。

一个非常简单的想法就是,既然你们都是分散的,那我把你这些SDF 合成一个大的低精度的 Global SDF ,这是对整个场景的表达。当然这里面的细节比较复杂,物体的移动,消失,增加,都需要提供一套 Update 的算法。今天就不展开了,如果有兴趣的同学可以自己研究。
不管怎么说,我们能形成一整个场景的 SDF 。如果有了整个场景的SDF,Raytracing 的速度就会非常快,因为它不再依赖于一个一个的物体。当然它的缺点是受制于存储空间,不能像 Per-Mesh SDF 那么精细,但它是一个非常好的加速的方法。


在 Lumen 里面,Global SDF 和Per-Mesh SDF 都使用到了。如果一开始你就使用Per-Mesh SDF,你可能要二百多次的物体测试,一旦有了Global SDF,物体的测试数量就会极大的下降,因为它可以快速的找到一些近处的点,然后再根据周边的Per-Mesh SDF去做。

Global SDF 在实际上的运算中它会做成四层 mip, 在 Camera 近处 SDF 精度高一点。远处我 SDF 精度低一点。因为SDF跟 Texture 一样是一个非常 Uniform 的表达,所以它天然的支持 Clip Map。
SDF虽然并不能直接用作渲染,但它可以把很多渲染的计算进行迅速的优化。使用 SDF我们可以快速的提供场景的表达,在这个表达到上面做 Raytracing的效率非常高,并且不依赖于硬件的Raytracing。

Phase 2 : Radiance Injection and Caching

那接下来我们要解决如何把光子注入到世界。


在Lumen里面,他搞了一套非常特殊的东西叫做 Mesh Card 。当我们计算全局光照的时候,从光的视角我们去照亮整个世界,其实整个世界里面无论你看得见的还是看不见的 pixel 它实际上都会被这个光给照亮。每一个被照亮的 pixel ,实际上也成为GI 的一个贡献者,就是我的小光源之一。
我们的Mesh SDF Trace虽然可以拿到对应的相交点,但是对应物体的材质信息是拿不到的,我们还需要知道物体表面的材质属性,但SDF 并不包含 Material 的Attribute。并且仅使用RSM也是不够的,因为它只有灯光看到的那些表面的信息,其余的很多的角度依旧看不见。
Lumen想到了一个方案,首先就是每个Mesh导入的时候,如上图所示,会生成Mesh Card,这样在使用Per-Mesh SDF的时候,就可以找到对应的MeshCard。

这是我们在 UE 5 里面搭的一个场景,你可以看到生成的 Card ,其主要作用是提供光照采样的位置和方向。

Cards只是Lumen捕获光线的位置,其可以离线生成,而存储其中的数据则必须实时捕获,因为不同的Mesh之间会有交叉和遮挡,因此就算是同样的材质渲染出的Cache结果也是不一样的。
所以我们没有办法在离线的时候把这个信息Cache住,而是在运行时给每个Card渲染出它的表面Cache,里面包含有Albedo,Opacity,Depth等各种信息。如果你带有自发光的话,也会把你的 Emissive也存储进去。
可以看到这里的Surface Cache的数据与 Deferred Shading 的 G-Buffer 很像,我们在计算光照的时候直接就可以通过这里的Cache数据来计算。

当离我的相机比较近的物体,Surface Cache的分辨率会高一点,相机远离的物体Surface Cache分辨率低一点。比如说我侧面有一个石头,这个石头可能只有两米高,但是它可能占据了我平面 1/3 的地方。那我平面近处的东西很可能会受这个石头的影响。远处可能有个雕像,它有 5 米高,但它距离我 100 米远,它的精度就可以低一点。
我们是以Atlas的方式去排布所有的Surface Cache在对应的4096x4096 的空间里。这里需要注意的是 Surface Cache并是一个单张的 Texture ,而是一系列的 Texture的集合。

Surface Cache内部的贴图都会进行一遍硬件支持压缩,这样可以减少显存的占用。

Surface Cache存储着Gbuffer 的信息,不过这还不够,因为我们不能再在相交点重新发射大量的光线然后不停的递归去计算间接光,我们希望把Radiance 固化在Surface Cache上面就如同Photon Mapping 的思路一样。
因此我们希望它Surface Cache上还能存储对应的Radiance信息,那就是Radiance Cache,有了 Radiance Cache,就可以在 Trace 时直接进行采样作为 Trace 方向对应的 Radiance。当然这里面会有两个重要的问题。

  1. 第一个就是 Surface Cache的某个点,这个光的照耀下它到底有多亮,它有可能会被 Shadow 挡住,那么我们怎么去知道这件事情呢。
  2. 第二件事情是,我既然知道你的GBuffer信息,直接光照我可以得到,但如果你是 Multi Bouncing 我怎么办?

如上图,最核心是要生成Surface Cache Final Lighting。

  1. 第一步,对于 Surface Cache 上的每个像素,我们可以很简单的计算 Direct Lighting 。对于刚才第一个问题,如果在Shadow里面,我们可以通过Shadowmap来做。
  2. 第二步,其实比较复杂,为了能够计算Indirect Lighting,我需要在 WorldSpace里面其实建了一批对光的 VoxelLighting的表达,这一步的 VoxelLighting并不是给当前帧用的,而是给下一帧使用。为什么要做这一步,并且VoxelLighting的具体细节将会在接下来的章节中讲述。
  3. 第三步,我们使用上一帧的第二步建立的Voxel Lighting,采样出对应的 Indirect Lighting ,把它和Direct Lighting 这两个合到一起,就变成了我的这一帧的这个 Final Lighting。

随着时间的积累,第一帧F0的时候只有一次 Bouncing , 第二帧F1 的时候,其实我就有这个两次 Bouncing 的值了。在F2时就具有三次Bouncing 的值。
Surface Cache Direct Lighting

第一步是计算Surface Cache上的直接光照,这是比较简单的,Surface Cache每一个 pixel我去找对应的Lighting,只要采样Shadow map 我就知道是否在阴影里。并且我也不用真的渲染Shadow map,实际上我只要用我的 SDF 查询一下灯光的可见性,就知道我这个点和灯光可不可见。

如果你有多光源,我就对于每一个Page的每个光源都算一遍,最后累加在一起。
World Space Voxel lighting

当我们解决了Surface Cache直接光照后,我们需要处理间接光照,间接光照如果在很近的地方,是可以使用Per-Mesh SDF来找到对应的Surface Cache进行更新,但是如果采样点很远,我们对于远处的物体并不会使用Per-Mesh SDF ,而是会使用 Global SDF Tracing,因此我们没有办法同 Per-Mesh SDF 一样从 Surface Cache 上获取 Material Attribute。
因此我们对于远处的物体需要构建一个针对 Global SDF tracing的结构。我们把整个场景以相机为中心,做了一个 Voxel的表达。所有需要 Globa SDF Ray tracing 的功能都需要采样 Voxel Lighting。

与传统的 Voxel Lighting 不同,Lumen 并不是体素化全部场景,而是体素化相机周围一定范围内的空间做一个Clip Map , Clip Map 里面有 4 层,每层 64x64x64 个Voxel,把它存储到一个 3D 的 texture 里面。这样,我们将 Lighting 注入到 Voxel 中,这样就以更粗的粒度记录了空间中的光照信息。

在之前我们讲VXGI 里面,它一般都是用保守光栅化的方法去构建Voxel。但是在 Lumen 里面,他的方法就更为巧妙,Lumen 将 Clipmap 又进行了网格化,将 4x4x4 的 Voxels 合并为一个 Tile,Clipmap 有 64x64x64 个 Voxels,因此每个 Clipmap 可划分为 16x16x16 个 Tiles。从每个Voxel的边上,随机的射一根Ray进去,如果我能打中任意一个 Mesh在我的这个Voxel里面,就说明这个 Voxel 不为空的。

每个Tile 顶多五六个物体,这样我们只需要跟大概四五个物体进行求交,运算效率会非常高的。

当我们有了整个空间的Voxel表达,我们现在要知道Voxel 的lighting。需要注意的是,每帧我们会重新的体素化更新所有的Voxel 的lighting。
实际Voxel 的数据是根据从 6 个方向分别采样与 Voxel 相交的 SDF,根据它采样的 Mesh Card 信息再从 Surface Cache 中的 Final Lighting 中采样 Irradiance。
第一帧F0的时候,Voxel是全黑的,这个时候没有Indirect Lighting,只有 Direct Lighting ,因此我们 Surface Cache 的 Final Lighting 实际上就是直接光照的结果,这时我们把Final Lighting 的结果注入到对应的Voxel Lighting中。
下一帧里,我就有了Voxel 的信息,虽然Voxel只有直接光的信息,不过没关系,我就用第一帧Voxel 的信息再去更新我的Final Lighting 。然后我再把Final Lighting的信息写到Voxel 里面。经过多帧的跌断,我取到的信息天然的就具有 Multi Bouncing 的结果,这个方法非常的巧妙。
有些比较复杂的东西,比如说 terrain 肯定不能用 Mesh Card 去表达;再比如中间有半透明的雾怎么去做处理。 Lumen 里面有无数的细节,我们作为 10 系列的课程也就不展开了。
Surface Cache Indirect Lighting

当我们有了整套对世界的表达,我们将要去做Indirect Lighting 的计算。Lumen将屏幕空间的 Surface Cache 划分为8x8像素的 Tile,在 Tile 上放置 Probe,每 Tile 可放置 2x2 个 Probe。每个 Probe 在半球方向上进行16次的 Cone Tracing,但我会存入8x8Trace 的结果,这个结果是需要16次的 Cone Tracing差值得到。

我们将会得到的的数据存成了SH, 因为我要根据采样点把16个点进行差值为64个点,所以使用SH来进行。

如图所示,直接光照其实是 HDR 所以你看上去它会偏亮,实际上的话它之间亮度差得很大。

这样,两个Lighting 就可以结合在一起,形成我们想要的这样的一个 Multiple lighting。
使用 Surface Cache还把一个很难的问题给解决掉了,就是自发光。大家在做游戏的时候,自发光其实很难处理。如果你只考虑它本身变量的话,那很简单,在最终的颜色上硬生生的加上去一个光。但是如果我们想自发光能够影响GI的话,这其实非常难。
虽然在前面我们讲了很多方法能解决 Multi Light source 问题,但是我这是一条光带,我怎么去渲染呢?如果使用Surface Cache,即使是一个光带,我实际上也能够把它整体Cache 到 Lighting 里面去。


对于整个 Surface Cache的更新还是非常耗时的。因此Lumen里规定,每帧最多不超过 1024 x 1024 个texels更新,对于间接光照因为它要在很多Voxels 上采样,因此最多更新512 x512 个texels。对于每一个 Mesh Card来讲,他都要排队请求自身更新。这里面其实有一整套的比较复杂的排队算法。

Phase 3 : Build a lot of Probes with Different Kinds

前面讲的内容依旧无法直接给我们渲染提供直接帮助,这一节我们将来看如何能够拿到对应像素点的间接光数据。
Screen Space Probe

对于最终的渲染来讲,我们需要拿到当前像素点法线半球面的所有Radiance,也就是对应 Radiance的一个 Probe 的表达。所以我们首先需要去分布这些Probe,最自然的一个想法是在空间上均匀分布,因为我既然已经有 Surface Cache 我又得到了一个 Voxel,我这样简单去构建整个 Probe 分布是可以的,比如说每隔一米放一个 Probe 。
但它并不能保证你能准确地表达光场的变化,如果这个变化如果你表达不了的话,实际上你渲染出来的东西看上去就很怪。就如我们经常用一句行话:这看上去很平。所有的预计算生成 Probe 的 方案都会产生类似的问题。
而Lumen就比较大胆,他说我就在 Screen Space 去洒 Probe 。如果我够狠一点的话,我屏幕上的每个像素点,我都对它进行整个Probe的 采样,这当然在结果上没什么问题。
但是Lumen还没有那么粗暴,它每隔 16x16 个 pixel ,去采样一个 Screen Space 的这个Probe 。因为实际上,近处位置16x16 个pixel 的范围在空间上相距不会太远,而间接光照非常低频,在这么近的距离里面,它的变化不会很大。

对于屏幕空间 Probe ,我们使用八面体映射(Octchedron mapping) 来进行球面坐标到2D纹理空间坐标的转换。虽然在球面上撒采样点最简单的方法就是按照经纬度采样,但是经纬度采样如果映射到一个 2d 的 texture 会出一个问题:极点附近的采样密度特别高,赤道附近的采样密度过低。而我们希望一个分布相对均匀的采样,并且这个采样需要满足,我给你任何一个方向,能迅速的知道它的 UV 空间的位置。因此我们选择使用Octchedron mapping来进行映射。

虽然Lumen每隔 16 个 Screen Pixel 放置一个Probe,但是我们放置完之后,还需要检查当前像素平面和对应Probe彼此之间是否在一个平面上。我们可以根据深度和Normal很容易得到平面信息。
如上图黄色部分就是那些对应Probe无法满足需求的像素。这时说明当前16 x16 的精度对他们来说不够,对于这些像素,我们会自适应地细化为8x8 像素的Probe,如果还不够的话,我们会继续细化为4x4。

每个pixel 渲染时,都要从临近的4个 Probe 之间去取它的插值,这时我需要通过法线和位置计算出自己的空间平面,把四个 Probe 的中心点投影到我的平面上,通过获得的投影距离来确认对应的光照权重。

我们根据投影权重会计算出一个 Error值,如果Error值累计大于某一个 threshold 我就认为这个Probe不能用了。如果我这些采样点很多都不能用的话,我认为就是你这四个采样点对我是无效的。这时,我就会进行自适应使用更细化的Probe来进行我的间接光插值计算。

Screen Space 的所有 Probe都放在一个 Atlas里面。由于我们的贴图都是正方形,但是我们的屏幕是长方形,因此天然就会一些贴图空间的冗余。 它会把你这些需要自适应的 Probe packing 在它的下面。因此它没用用到额外的存储空间就做到了Adaptive 的采样。

如上图,我们是把Lumen的Screen Space Probe 打印了出来,暗红色的点是 16x16 ,黄色 8x8 或者 4x4 的。

我们在进行屏幕空间 Probe采样的时候,用了 jitter 来防止过于重复。 多次jitter的结果在时序上又变成了一次Multi Bouncing 的采样。
Importance Sampling
我们如今已经知道如何去分布我们的 Probe ,接下来我们就要去采样。之前讲过,我们如何使用Uniform 的采样会导致一些问题,在一个房间里,如果不知道窗户在哪里,采样如果不是使劲的朝着窗户方向去采的话,结果就会像秃头般一样的,黑一块白一块非常丑。

如果不进行important Sampling 我们看到了 lighting 的结果大概如上图所示。

因此我们最重要的一件事情是找窗户,我要尽可能往窗户的那个方向多射一些Ray,也就是说你需要环境感知。


如之前的章节所说,我要尽可能让我的这个概率函数P符合上面函数的分布。上面这个函数是两个函数的积,其中一个是光,另一个是我表面的BRDF。所以首先你需要知道光在哪儿,第二个你需要知道法线在哪,因为背面的光对我来说毫无意义。

如何得到光的方向呢,虽然并不知道这一帧的光在哪,但是我做一个假设,就是光的变化没有那么快。因此我们可以把上一帧的 Probe 采一遍,重投影上一帧周围四个的Probe的值,把对应SH的信息画在一个 8x8 的图里,这个图中亮的地方就是光比较亮的地方。这样我就大概知道哪个地方亮,哪个地方暗。当然如果上一帧这里不可见的时候,会出现有失败的情况。我们会Fallback到世界空间的Radiance Cache里面

第二点对于BRDF的部分,大家天然的能够想到,沿着 Normal 做一个 Cosine Lobe 。这听上去非常合理。但是这里面有一个很大的错误,那就是像素的Normal 非常非常高频。比如一个小区域中可能有一千多个pixel ,那一千多个 pixel 加权的法线朝向并不能由我这单独采样点的法线所代表。


如果真的把 1024 个 Pixel 的 Normal 全部加在一起也太夸张了,因此我们使用64 个采样点去估计,并且不使用随机采样,它会根据Depth的权重,确保这些采样点和当前像素点的Depth彼此相差不要太大。然后我把这些 Normal 的Consin lobe 全部积分在一起。

Lumen 采用了一种称为结构化重要性采样(Structured Importance Sampling)的机制,其核心思想是把不重要的采样分给重要的采样部分。

当我们知道了Lighting和BRDF的贡献值,我们可以对所有采样点进行排序,排列出哪些点是重要的,哪些点是不重要的。这个时候我们设置一个阈值,找出排名靠后的三个最不重要的方向,假设他们的这个 PDF 值都小于我设定的阈值的时候,我们就不去采样这三个方向,把这三个采样留给我最需要采样的方向,我就可以对我最需要采样的方向进行一次 Super Sampling。
依此类推,通过这个 PDF 的值,把最不重要的方向全部过滤掉,然后让我的采样尽量集中在重要的方向,这个方向可能来自于你的法线方向也可能来自于光源。这样,就有了固定开销的Adaptive Sampling。

如图所示,右边使用Structured Importance Sampling,它的光线会集中在墙上相对比较亮的地方,整个 Rendering 的结果就会好很多。


如图所示,左边是没有做important Sampling,右面是做了这个important Sampling。


这张图同样如此。
Denoising and Spatial Probe Filtering
接下来我们需要进行降噪,做 GI 的话Filtering 就是你逃不掉的东西。

按照16x16 Pixel的 Probe 得到的信息是非常不稳定的。因此我们使用周围3x3的 Probe 来做Filtering。


每一个Probe都射出去了 64 根Ray,如果我把临近Probe同方向的Ray的光照结果,直接加到一起其实是不对的。
因为Neighbor Probe 和Current Probe存在一定距离,Neighbor Probe相同方向射到的物体在Current Probe 看过去的射线角度可能完全不一样。如上图所示,有两个Probe,他们相同方向上有两根Ray,一个蓝色一个灰色,蓝色射中的物体在Current Probe视角下并不是一个方向。
所以当我们去加权这些 Ray 的时候,对于所有的 Neighbor Probe 的Ray 都要做一次可用性检测。如果这个夹角超过了10 度,我就不用了。

如图所示,如果这个东西不处理,这个墙上会有很多的 noise 。

仅仅处理角度还不够,假设我Neighbor Probe的Ray交点非常的远,但Current Probe射到的距离很近的,虽然角度是对的,但不好意思,我认为这种情况也是无效的,我还是只用我自己的数据。


这个问题不解决的话,它就会出现这种漏光的问题。如上图左边,你发现那个毛巾的这个内侧面,和它靠近墙的地方。如果你不考虑这个差值,很多光就会漏进来。
World Space Probes and Ray Connecting
Screen Space Probe如果它跑的太远,效率会很低。因此Lumen希望Screen Space Probe 你只处理周边的东西。


对于远处的物体,我们会在 World Space 里面预先放好一些 Probe ,我们把远处的lighting 都Cache在里面,这样当 Screen Space Probe 取一个方向的Ray的时候,就可以在沿途的 World Space Probe 里面的光线给你取出来。
如果你的场景是一个相对静态的场景,光源也基本固定,但你的相机仍会走来走去,因此你 Screen Space Probe 一直是不稳定的,每一帧都要去更新。而World Space Probe 使用 Clip Map 的方法去部署,我在运动的时候只需要在边缘处增加几个 Probe 后面删掉几个Probe 就可以了。

如上图,我们可以看到World Space Probe的分布。Screen Space 在整个球面上的采样是 8x8 ,而World Space Probe作为最后的救赎者,采样数是32 乘32,差不多 1000 多根Ray。有了World Space Probe,很多时候Screen Space Probe 就不用跑得很远,它只要跑到附近的 World Space Probe里面就去借他的光就可以了。

那么我们如何我把光怎么接起来呢。 Screen Space Probe的Ray只会在近处采样,当我走到临近的World Space Probe 的时候,我就罢工了,远处剩下的内容我只需要去World Scape Probe取就可以了。


World Space Probe的覆盖范围形成一个 Bounding,Screen Space Probe 做 Raytracing的时候,只会走Bounding对角线长度的两倍.
距离相机较近的地方的Screen Space Probe周围的World Space Probe密度会高一点,如果你到了远处,World Space Probe的范围其实已经比较大了,所以那个时候Screen Space Probe Ray 跑的距离就会比较远。
而World Space Probe的Ray在采样的时候也会 Skip 掉自己的对角线长的距离,因为它没有必要采样近处的内容,近处的只需要交给 Screen Space Probe就可以了。

这里面讲一个很有意思的 Artifact , Screen Space Probe 射出一根Ray,World Space Probe如果沿着 Screen Space Probe 那个同样的方向去找,就会产生一个很有意思的问题。

World Space Probe很可能会跳过靠近我的一个阻挡物,这个时候就会出现漏光的问题。因此我们需要让光线弯曲一下。

做渲染的时候我们其实不是那么特别 Care 物理的完全正确,需要光线拐弯的时候,它光线就给我拐弯。我们求Screen Space Probe发出的射线到World Space Probe范围边缘的交点,我用交点和Screen Space Probe中心构造一个新的方向,然后我用这个方向当做World Space Probe采样的方向,虽然光转弯了,但它确实能解决一部分的漏光的问题。

最终渲染时,我们还是只使用 Screen Space Probe去渲染, World Space Probe只是帮助我们去快速的获取远处的光线。 因此如果World Space Probe 对应的空间内没有物体,也不在我的 Screen Space 里面,这些地方的 Probe 是不需要采样的。
也就是说,只有那些对 Screen Space Probe 有差值需求的World Space Probe才会去更新。我们会把Screen Space Probe周围八个 World Space Probe标记为 marked 。那只有这些 marked 的 World Space Probe 才有必要进行采样。


如上图,如果你只是用Screen Space Probe 的话,它如果只有两米,你看到的结果大概是这个样子的。

但是如果你有World Space Probe,你可以看到这个光看上去就准确的多了。

Phase 4 : Shading Full Pixels with Screen Space Probes


虽然我们做了 Important Sampling ,但实际上 Indirect Lighting还是很不稳定,因此我们把这些光全部投影到 SH 上面去。 SH 本质上相当于对我们的整个 Indirect Lighting进行了一个低通滤波,把它变成了一个低频信号,用它来做 Shading 的时候看上去就柔和了非常多。


这就是我们最终能够得到的结果。

Overall, Performance and Result


对于不同的 Raytracing方案,硬件上的成本是不一样的。最快的tracing就是基于 Global SDF 。其次就是在屏幕空间我进行 linear Screen 去插值。那么比它稍微慢一点的就是 Per-Mesh SDF 。 HZB 它稍微比 linear Screen 要慢,但是它的准确度其实会更高。硬件Raytracing的准确度肯定是最高的,但是它开销会更高。


我们通过这张图可以看到Lumen使用了混合的Tracing方法。红色区我们使用 Screen Space 的Tracing;绿色的区域用 Per-Mesh SDF ;蓝图的部分也就是说更远的地方,我只能用 Global SDF 。
如果每一个 Pixel Tracing使用的方法是单一的,我们应该看到的是纯色图,但现在我们看到的图是渐变图。这其实也说明我们每个Probe采样中,混合使用了各种方法 。


我们希望越靠前的方法,准确性越高。

  1. 我们首先是使用Screen Space Trace 。它基于HZB走50 步,如果能 Trace 到,我就把这个结果拿过来。
  2. 如果 Screen Space Trace失败,我们就会使用最主要的Per-Mesh SDF的方案。 Per-Mesh SDF Trace 的距离非常近,只有 1.8 米。这个时候我可以返回Mesh ID,可以直接拿到Surface Cache。
  3. 再远一点,我们只用 Global SDF, 而Global SDF只能拿到Voxel Lighting 。
  4. 如果 Global SDF 也失败了,你就采到CubeMap上去。



这里我们需要重点提一下SSGI,如果没有SSGI只有Lumen的话,可以看到下面那个倒影其实很粗糙。因此对于近处高频的物体,SSGI 还是蛮重要的。



Lumen最了不起的地方,还是在工程上真的完成了交付。在 PS5 的这个硬件平台上,能做到 3.74 毫秒。如果你愿意降低采样分辨率,你的效率可以更高,可以从将近3.74 毫秒下降到 2.15 毫秒。



所以16x16 的 pixel 的选择,我相信作者自己肯定也做了大量的尝试,得出的一个兼顾性能和效果的参数。



Lumen对整个动画电影行业,都有着非常巨大的影响,将室内设计师,效果图公司所梦寐以求的离线渲染的效果,做到了可以实时产生,非常的了不起。。
Lumen也奠定了未来 10 年下一代的游戏引擎的渲染标杆,我们认为 GI是下一代顶级游戏引擎的标配,你只要做下一代游戏引擎,你的 GI 必须是实时的。
Lumen 只是这一系列伟大征程的开始,你可以看到Lumen 基于现有的硬件, 还是做了大量的妥协。未来十年随着硬件的发展, 实时GI 将会变得更加成熟,也会变得更加简洁。而Lumen 是一方向的开山鼻祖,他的创作者必然会载入史册。


【转载】1000本的计算机经典书籍分享

超过1000本的计算机经典书籍分享

微信群 公众号 知乎 CSDN 牛客网 八股文 学习 免费PDF

希望阿秀开发的网站能够对你有所帮助!祝你求职顺利!网站链接https://top.interviewguide.cn/

1、👉 如果国内访问Github网速较慢,我在码云也放了一份书籍资源,国内访问速度更快,点此直达 

本仓库持续更新中,后续会陆续分享更多经典电子书,强烈建议大家 Star 下本仓库,下次找书可以直接 Ctrl + F,找书再也不愁 !

简单附赠两个云服务器优惠信息:

【阿里云】云服务器狂欢特惠,2核2G5M 轻量级应用服务器 60 元/年【阿秀目前使用】 传送门

【腾讯云】轻量应用服务器限时秒杀,1核2G6M 限时首年74元/年【阿秀强烈推荐】 传送门

2、🔥🔥🔥计算机校招求职学习网站

我把自己秋招时的上岸字节跳动的学习笔记做成一个网站了,适用于计算机校招应届生以及毕业三年之内的社招求职党传送门

3、😄学习计算机八九年收集到的一些不错的资料免费分享,包括清华北大计算机课程资源等,点此一键白嫖

4、❤️走心内推!字节跳动核心岗位在职员工校招社招内推方式,传送门

5、👍🏻两位字节跳动后端研发工程师学习笔记,点此一键免费获取

6、⭐计算机经典图书榜单Top50&互联网一二线中厂校招面试PDF合集,点此一键免费获取

7、😜发现一个相当不错的计算机各类种语言&学科学习路线仓库, 点此查看

8、赞赏

这些书籍基本都是我从一个盗版电子书网站上收集到的,网址是:kanshuy1234.com,现在分享出来希望能对大家有所帮助,自己也花了很久时间整理出来的。
如果觉得本仓库有用,赏赐一块钱,买杯奶茶喝可好?感谢您了~

[TOC]

点击下列目录直接跳转书籍所在类别,但有时目录跳转会失灵…如果没有没有正常跳转可以动动小手向下拉即可,全部的书籍都在本页面。

笔者是一个人在维护这个仓库,本地大概是1100多本书了,需要一步步慢慢上传,我只能在闲暇时间慢慢更新,目前已经更新超过500+了,如果没有您要用的书,可以加一下**个人微信(coderxiu2)**,注明来意,我会慢慢添加上去的。一个人的力量是有限的,请谅解一下。

00、C语言

01、C++

02、Java

2.1、Java基础到进阶
  • 《深入分析JavaWeb技术内幕》 百度云链接 提取码:g1qz
  • 《深入剖析Tomcat》 百度云链接 提取码:o06x
  • 《Effective Java 2nd Edition 英文版》 百度云链接 提取码:czeq
  • 《Effective Java 中文第二版》 百度云链接 提取码:5p9e
  • 《Java性能优化权威指南》 百度云链接 提取码:9g85
  • 《Java核心技术(卷1)第8版》 百度云链接 提取码:pmx2
  • 《Java核心技术(卷2)第8版》 百度云链接 提取码:in8y
  • 《Head First Java第二版涵盖java5.0》 百度云链接 提取码:qfz2
  • 《Java 8 实战》 百度云链接 提取码:3ej1
  • 《阿里巴巴Java开发手册、1.0.0公开版》 百度云链接 提取码:pl9v
  • 《阿里巴巴Java开发手册、1.1.0正式版》 百度云链接 提取码:zrxy
  • 《阿里巴巴Java开发手册、1.2.0升级版》 百度云链接 提取码: p350
  • 《阿里巴巴Java开发手册、1.3.0终极版》 百度云链接 提取码:ycoc
  • 《阿里巴巴Java开发手册、1.4.0详尽版》 百度云链接 提取码:dya6
  • 《阿里巴巴Java开发手册、华山版》 百度云链接 提取码:z3fh
  • 《阿里巴巴Java开发手册-嵩山版》 百度云链接 提取码:z8mf
  • 《阿里巴巴Java开发手册、泰山版》 百度云链接 提取码:6cxz
  • 《Java编程思想(第四版)》 百度云链接 提取码:ogz5
  • 《Java并发编程的艺术》 百度云链接 提取码:v8pj
  • 《JAVA并发编程实践》 百度云链接 提取码:se4k
  • 《Java从小白到大牛精简版》 百度云链接 提取码:zmer
  • 《Java核心技术卷1基础知识原书第10版》 百度云链接 提取码:c6d5
  • 《Java核心技术卷2高级特性原书第10版》 百度云链接 提取码:gjlh
  • 《码出高效:Java开发手册》 百度云链接 提取码:4i0f
  • 《阿里巴巴Java开发手册v1.2.0》 百度云链接 提取码:129f
  • 《深入理解Java虚拟机:JVM高级特性与最佳实践(最新第二版)》 百度云链接 提取码:qbfq
  • 《Camel in Action》 百度云链接 提取码:kcs0
  • 《Head First Servlet and JSP(高清中文版)》 百度云链接 提取码:q55k
  • 《Maven实战(高清完整带书签)》 百度云链接 提取码:4q4a
  • 《Spring 5 Recipes, 4th Edition》 百度云链接 提取码:o3uk
  • 《Spring in action 中文版(第4版)》 百度云链接 提取码:daya
  • 《轻量级JavaEE企业应用实战》 百度云链接 提取码:1rn2
  • 《ThinkInJava(JAVA编程思想)》 百度云链接 提取码:e5rp
  • 《Head First Java 中文高清版》 百度云链接 提取码:yry8
  • 《Netty in Action第五版》 百度云链接 提取码:ty0o
  • 《Java核心知识点整理》 百度云链接 提取码:3s2p
  • 《Java并发编程实践 英语版》 百度云链接 提取码:9mp0
  • 《Effective Java(中文版第3版)》 百度云链接 提取码:ruo1
  • 《Netty实战》 百度云链接 提取码:rd2m
  • 《Spring实战(第4版)》 百度云链接 提取码:7kfk
  • 《java从入门到精通(第4版)》 百度云链接 提取码:movm
  • 《Spring.in.Action.5th.Edition》 百度云链接 提取码:le2h
  • 《Spring 技术内幕》 百度云链接 提取码:gni3
  • 《Java EE框架整合开发入门到实战:Spring+Spring MVC+MyBatis(微课版)》 百度云链接 提取码:asxu
  • 《大型网站技术架构:核心原理与案例分析》 百度云链接 提取码:q0ua
  • 《大话java性能优化》 百度云链接 提取码:hfeg
  • 《分布式Java应用基础与实践》 百度云链接 提取码:dv70
  • 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)+(华章原创精品)+-+周志明》 百度云链接 提取码:rs4z
  • 《Java核心技术(卷I)基础知识(原书第9版)》 百度云链接 提取码:4d76
  • 《深入分析Java Web技术内幕 旧版》 百度云链接 提取码:8s2v
  • 《SSM企业级框架实战》 百度云链接 提取码:h5p2
2.2、Java 大数据
  • 《大数据架构师指南》 百度云链接 提取码:vi00
  • 《大数据之路:阿里巴巴大数据实践》 百度云链接 提取码:xbco
  • 《深入理解Spark:核心思想与源码分析》 百度云链接 提取码:rkov
  • 《图解Spark 、 核心技术与案例实战》 百度云链接 提取码:kfaj
  • 《Apache+Kylin权威指南》 百度云链接 提取码:y31o
  • 《Hadoop The Definitive Guide》 百度云链接 提取码:ws9o
  • 《Hadoop技术内幕:深入理解MapReduce架构设计与实现原理》百度云链接 提取码:afhy
  • 《Kafka & Mafka技术分享及讨论》 百度云链接 提取码:4u7i
  • 《Scala编程中文版(33章全)》 百度云链接 提取码:6m1n
  • 《Spark大数据处理:技术、应用与性能优化(全)》 百度云链接 提取码:ghw3
  • 《Spark机器学习》 百度云链接 提取码:3li7
  • 《Spark快速大数据分析》 百度云链接 提取码:jlqc
  • 《Spark最佳实践》 百度云链接 提取码:ziy5
  • 《ClickHouse中文文档》 百度云链接 提取码:vjb3
  • 《Flink基础教程》 百度云链接 提取码:7ttp
  • 《CDH集群运维手册》 百度云链接 提取码:38qp
  • 《Elasticsearch.权威指南(中文版)》 百度云链接 提取码:fmo0
  • 《Hadoop应用架构》 百度云链接 提取码:ezgg
  • 《Elasticsearch集成Hadoop最佳实践》 百度云链接 提取码:eiro
  • 《Apache Spark源码剖析》 百度云链接 提取码:pe9u
  • 《Flume构建高可用、可扩展的海量日志采集系统》 百度云链接 提取码:qxc4
  • 《HBase_权威指南》 百度云链接 提取码:y8bm
  • 《HBase实战中文版》 百度云链接 提取码:6tek
  • 《Hive编程指南》 百度云链接 提取码:a88l
  • 《HBase不睡觉书 带目录(高清)》 百度云链接 提取码:wpmv
  • 《Apache Kafka实战》 百度云链接 提取码:1zdo
  • 《Mahout算法解析与案例实战》 百度云链接 提取码:0vtf
  • 《Kafka权威指南(2018中文版)》 百度云链接 提取码:nwrk
  • 《Kafka源码解析与实战》 百度云链接 提取码:t83m
  • 《MapReduce设计模式 [美.迈纳.舒克著]》 百度云链接 提取码:flxj
  • 《Spark快速数据处理》 百度云链接 提取码:4m6s
  • 《spark开发基础之Scala快餐》 百度云链接 提取码:hxz0
  • 《Lucene实战(第2版)》 百度云链接 提取码:uot4
  • 《Spark最佳实践陈欢,林世飞著》 百度云链接 提取码:nmmq
  • 《Spark大数据分析核心概念技术及实践OCR》 百度云链接 提取码:zbvo
  • 《Storm技术内幕与大数据实践》 百度云链接 提取码:duju
  • 《Spark内核设计的艺术架构设计与实现(耿嘉安)》 百度云链接 提取码:xuww
  • 《Presto技术内幕》 百度云链接 提取码:tsw5
  • 《决战大数据》 百度云链接 提取码:eu4p
  • 《Apache Kafka源码剖析》 百度云链接 提取码:qryy
  • 《从Paxos到Zookeeper 分布式一致性原理与实践》 百度云链接 提取码:6iyn
  • 《可视化数据》 百度云链接 提取码:f29x
  • 《基于Apache Kylin 构建大数据分析平台》 百度云链接 提取码:vn2b
  • 《从零开始学Storm》 百度云链接 提取码:jfrn
  • 《最全的大数据解决方案》 百度云链接 提取码:me79
  • 《深入学习MongoDB》 百度云链接 提取码:4qzy
  • 《大数据技术原理与应用概念、存储、处理、分析与应用(第2版)》 百度云链接 提取码:h10h
  • 《机器学习与数据挖掘方法和应用(经典)》 百度云链接 提取码:ry6j
  • 《大数据Spark企业级实战版》 百度云链接 提取码:v6im
  • 《实战Elasticsearch、Logstash、Kibana:分布式大数据搜索与日志挖掘及可视》 百度云链接 提取码:yrpe
  • 《深入理解Spark 核心思想与源码分析 耿嘉安著》 百度云链接 提取码:z2eu
  • 《相关性搜索:利用Solr与Elasticsearch创建智能应用》 百度云链接 提取码:its4
2.3、Java工具

03、Python

3.1、Python书籍
3.2、Python爬虫
  • 《Python3网络爬虫数据采集》 百度云链接 提取码:jkk3
  • 《精通Scrapy网络爬虫、刘硕》 百度云链接 提取码:0f1v
  • 《用Python写网络爬虫》 百度云链接 提取码:657p
  • 《Python和数据分析》 百度云链接 提取码:wmlk
  • 《Data Structures and Algorithms in Python [Goodrich, Tamassia & Goldwasser 2013-03-18]》 百度云链接 提取码:41qe
3.3、Python数据分析
  • 《B站-七周成为数据分析师》 百度云链接 提取码:iw7k
  • 《B站-Python3数据分析与挖掘建模实战》 百度云链接 提取码:q43k
  • 《B站_用Tableau做数据分析》 百度云链接 提取码:v1l2
  • 《Python for Data Analysis》 百度云链接 提取码:fdla
  • 《Python金融大数据分析》 百度云链接 提取码:8bb7
  • 《Python科学计算与数据分析》 百度云链接 提取码:yu6t
  • 《Python数据分析基础》 百度云链接 提取码:n5et
  • 《利用Python进行数据分析》 百度云链接 提取码:o5jb
  • 《Python数据分析基础教程:NumPy学习指南(第2版)》 百度云链接 提取码:lq48
  • 《Python数据分析实战_2016版》 百度云链接 提取码:0yue
  • 《Python数据分析与挖掘实战》 百度云链接 提取码:8qjq
  • 《Tableau:数据可视化之极速BI》 百度云链接 提取码:59ey
  • 《Tableau:数据可视化之极速BI数据源》 百度云链接 提取码:kdrw
  • 《Tableau商业智能与可视化应用实战》 百度云链接 提取码:ptkp
  • 《Python数据分析14天入门训练营》课程资料 百度云链接 提取码:sr2z
  • 《触手可及的大数据分析工具:Tableau案例集 数据源》 百度云链接 提取码:ff3n
  • 《触手可及的大数据分析工具 Tableau案例集》 百度云链接 提取码:pjym
  • 《大数据分析精品资料》 百度云链接 提取码:fs3o
  • 《利用Python进行数据分析-第二版》 百度云链接 提取码:2hdl
  • 《人人都是数据分析师:Tableau应用实战》 百度云链接 提取码:yfq7
  • 《数据分析实战》 百度云链接 提取码:7sw2
  • 《数据分析修炼手册》 百度云链接 提取码:qfs7
  • 《数据分析学习顺序》 百度云链接 提取码:9dy8
  • 《增长黑客_创业公司的用户与收入增长秘籍、范冰》 百度云链接 提取码:llq2
3.4、Python数据可视化
3.5、Python数据挖掘
  • 《Python编程-数据挖掘入门与实践(中文完整版)-图灵程序》 百度云链接 提取码:22c2

  • 《Python数据挖掘入门与实践》 百度云链接 提取码:1r46

  • 《数据挖掘导论、完整版》 百度云链接 提取码:c8dx

  • 《数据挖掘原理与算法》 百度云链接 提取码:yx67

04、Go语言

05、数据结构与算法

06、操作系统

07、Linux

  • 《Linux 内核设计与实现》 百度云链接 提取码:1f28

  • 《Linux UNIX系统编程手册、上》 百度云链接 提取码:htnh

  • 《Linux UNIX系统编程手册、下》 百度云链接 提取码:lxd4

  • 《UNIX网络编程卷1:套接字 API》 百度云链接 提取码:3g7h

  • 《UNIX网络编程卷2:进程间通信》 百度云链接 提取码:7anx

  • 《UNIX环境高级编程 第三版》 百度云链接 提取码:eb50

  • 《《UNIX系统编程手册 上》.((德)Michael Kerrisk ).[PDF]@ckook》 百度云链接 提取码:2gkj

  • 《《UNIX系统编程手册 下》.((德)Michael Kerrisk ).[PDF]@ckook》 百度云链接 提取码:wtyp

  • 《Linux宝典》 百度云链接 提取码:225z

  • 《Linux常用命令大全》 百度云链接 提取码:8b4u

  • 《LINUX防火墙(原书第3版)》 百度云链接 提取码:o9md

  • 《Linux高级程序设计中文第三版杨宗德、人电出版社》 百度云链接 提取码:4cms

  • 《Linux环境编程:从应用到内核》 百度云链接 提取码:8v8w

  • 《Linux内核设计与实现 第3版》 百度云链接 提取码:vzmc

  • 《Linux内核完全注释》 百度云链接 提取码:wr95

  • 《Linux系统命令及shell脚本实践指南Linux unix技术丛书》 百度云链接 提取码:gygv

  • 《Unix & Linux 大学教程》 百度云链接 提取码:thfb

  • 《汇编语言基于Linux环境第3版》 百度云链接 提取码:0357

  • 《鸟哥的Linux私房菜基础篇和服务器篇》 百度云链接 提取码:kbpi

  • 《深入Linux内核架构 (图灵程序设计丛书·Linux UNIX系列)》 百度云链接 提取码:6int

  • 《深入理解Linux内核》 百度云链接 提取码:sm3l

  • 《[Linux命令详解词典].施威铭研究室.扫描版》 百度云链接 提取码:oc3u

  • 《Advanced Programming in the UNIX® Environment》 百度云链接 提取码:sm6b

  • 《Harley Hahn’s Guide to UNIX and Linux》 百度云链接 提取码:99c1

  • 《Linux Kernel Development》 百度云链接 提取码:rvnn

  • 《Linux多线程服务端编程 书签高清非扫描、陈硕》 百度云链接 提取码:5kq1

  • 《Linux网络编程》 百度云链接 提取码:vdbg

  • 《Linux高性能服务器编程》 百度云链接 提取码:jt32

  • 《Unix-Linux编程实践教程》 百度云链接 提取码:w5p1

  • 《UNIX编程艺术-中文版【The+Art+of+UNIX+Programming】》 百度云链接 提取码:8hhc

08、后端架构

09、计算机系统知识

  • 《大话计算机》 百度云链接 提取码:9jc8

  • 《编码:隐匿在计算机软硬件背后的语言》 百度云链接 提取码:60p1

  • 《程序是怎样跑起来的》 百度云链接 提取码:u30m

  • 《计算机是怎样跑起来的》 百度云链接 提取码:uwfk

  • 《程序员的自我修养—链接、装载与库、书签目录版本》 百度云链接 提取码:ivmv

  • 《计算机组成与设计:硬件_软件接口》 百度云链接 提取码:dhda

  • 《Patterson-Computer Organization and Design_The Hardware_Software Interface》 百度云链接 提取码:zn24

  • 《Code、The Hidden Language of Computer Hardware and Software》 百度云链接 提取码:pytl

  • 《计算机组成 结构化方法 中文 第6版》 百度云链接 提取码:c1qy

10、计算机网络

11、数据库

12、前端&全栈

13、安卓IOS逆向

14、机器学习&深度学习&人工智能

14.1、机器学习
14.2、深度学习
  • 《Deeplearning深度学习笔记最新版》 百度云链接 提取码:xn7v
  • 《Python深度学习》 百度云链接 提取码:e2ia
  • 《常用推荐算法(50页干货)》 百度云链接 提取码:o747
  • 《动手学深度学习-李沐》 百度云链接 提取码:xefc
  • 《深度学习(最全的中文版)_2017年新书》 百度云链接 提取码:rjtr
  • 《深度学习500问》 百度云链接 提取码:emzm
  • 《深度学习_中文版》 百度云链接 提取码:ippy
  • 《深度学习框架》 百度云链接 提取码:sko2
  • 《深度学习入门:基于Python的理论与实现.pdf+代码》 百度云链接 提取码:vub4
  • 《神经网络与深度学习》 百度云链接 提取码:ifjq
  • 《【高扬】白话深度学习姊妹篇、白话大数据与ML》 百度云链接 提取码:kra4
  • 《Keras》 百度云链接 提取码:4d41
  • 《Keras中文手册》 百度云链接 提取码:cncz
  • 《Programming-PyTorch-for-Deep-Learning.Creating-》 百度云链接 提取码:sss5
  • 《Tensorflow 实战Google深度学习框架(完整版pdf)》 百度云链接 提取码:pvkt
  • 《TensorFlow斯坦福大学-深度学习基础教程[aibbt.com]》 百度云链接 提取码:5lgw
  • 《TensorFlow 官方文档中文版、v1.2[aibbt.com]》 百度云链接 提取码:wkid
  • 《TensorFlow技术解析与实战.李嘉璇》 百度云链接 提取码:kw18
  • 《面向机器智的TensorFlow实践》 百度云链接 提取码:jid6
  • 《TensorFlow面向机器智能的TensorFlow实践 (智能系统与技术丛书)_[aibbt.com]》 百度云链接 提取码:a4oa
  • 《TensorFlow实战_黄文坚(完整)[aibbt.com]》 百度云链接 提取码:eimo
  • 《新版深度学习》 百度云链接 提取码:l60i
14.3、OpenCv
14.4、计算机视觉(CV)
14.5、自然语言处理(NLP)
  • 《deep_learning_for_nlp》 百度云链接 提取码:fhq3
  • 《Embedding Methods for NLP emnlp tutorial》 百度云链接 提取码:0bcg
  • 《Python自然语言处理中文翻译 NLTK 中文版》 百度云链接 提取码:cew3
  • 《Writing Code for NLP Research-1》 百度云链接 提取码:vexm
  • 《斯坦福CS224n 自然语言处理与深度学习、笔记 hankcs》 百度云链接 提取码:hihe
  • 《语音与语言处理Speech+and+Language+Processing》 百度云链接 提取码:hweq

15、设计模式

16、汇编语言

  • 《x86汇编语言:从实模式到保护模式_书籍及配套资料》 百度云链接 提取码:ne7j
  • 《汇编语言_王爽及相关资料》 百度云链接 提取码:cx9w
  • 《老“码”识途 从机器码到框架的系统观逆向修炼之路》 百度云链接 提取码:b773
  • 《80x86汇编语言程序设计教程》 百度云链接 提取码:eogv

17、Git

18、数学相关

19、程序员软技能

  • 《码农翻身:用故事给技术加点料_刘欣》 百度云链接 提取码:t2di

  • 《奔跑吧,程序员:从零开始打造产品、技术和团队》 百度云链接 提取码:yx4j

  • 《代码大全2中文版》 百度云链接 提取码:0lml

  • 《代码整洁之道》 百度云链接 提取码:3c81

  • 《黑客与画家》 百度云链接 提取码:1i8c

  • 《浪潮之巅(完整版)》 百度云链接 提取码:ikcl

  • 《敏捷软件开发:原则、模式与实践》 百度云链接 提取码:sw72

  • 《重构:改善既有代码的设计(第2版)》 百度云链接 提取码:kbup

  • 《how-to-be-a-programmer-cn》 百度云链接 提取码:ns57

  • 《华为大数据应用开发指南》 百度云链接 提取码:2snr

  • 《华为金融大数据解决方案汇报V2.0》 百度云链接 提取码:s9zi

  • 《人月神话-中文-目录-文字版》 百度云链接 提取码:vumo

  • 《孙金城丨阿里巴巴-Blink SQL关键技术及实现原理》 百度云链接 提取码:6mjd

  • 《程序员健康指南》 百度云链接 提取码:0bxr

  • 《概念与技术(第3版)》 百度云链接 提取码:oolp

  • 《技术之瞳+阿里巴巴技术笔试心得》 百度云链接 提取码:978f

  • 《阿里双十一技术》 百度云链接 提取码:y5dd

  • 《程序开发心理学》 百度云链接 提取码:1a20

  • 《大数据之路:阿里巴巴大数据实践》 百度云链接 提取码:8uvo

  • 《软件工程-实践者的研究方法.8th》 百度云链接 提取码:qb99

20、其余资料

21、社招校招汇总

22、我建议你下载,以后肯定用得上


免责声明

本仓库书籍链接全部来源于网络其他人的整理的链接,个人只是搜录整理他人成果。

如有疑问请提交issue,有违规侵权,请联系本人 coderaxiu@163.com ,本人立马删除相应链接,感谢!

本仓库仅作学习交流分享使用,不作任何商用。


卷积神经网络实现图片验证码

cnn_captcha

use CNN recognize captcha by tensorflow.
本项目针对字符型图片验证码,使用tensorflow实现卷积神经网络,进行验证码识别。
项目封装了比较通用的校验、训练、验证、识别、API模块,极大的减少了识别字符型验证码花费的时间和精力。

项目已经帮助很多同学高效完成了验证码识别任务。
如果你在使用过程中出现了bug和做了良好的改进,欢迎提出issue和PR,作者会尽快回复,希望能和你共同完善项目。

如果你需要识别点选、拖拽类验证码,或者有目标检测需求,也可以参考这个项目nickliqian/darknet_captcha

时间表

2018.11.12

初版Readme.md

2018.11.21

加入关于验证码识别的一些说明

2018.11.24

优化校验数据集图片的规则

2018.11.26

新增train_model_v2.py文件,训练过程中同时输出训练集和验证集的准确率

2018.12.06

新增多模型部署支持,修复若干bug

2018.12.08

优化模型识别速度,支持api压力测试和统计耗时

2019.02.19

  1. 新增一种准确率计算方式
  2. TAG: v1.0

2019.04.12

  1. 只保留一种train_model.py文件
  2. 优化代码结构
  3. 把通用配置抽取到sample_config.jsoncaptcha_config.json
  4. 修复若干大家在issue提出的问题

2019.06.01

  1. 完善readme文档,文档不长,请大家一定要读完~
  2. 使用cnnlib目录存放神经网络结构代码
  3. 做了一版训练数据统计,大家可以参考我们的训练次数、时长和准确率
  4. TAG: v2.0

目录

[toc]

1 项目介绍

1.1 关于验证码识别

验证码识别大多是爬虫会遇到的问题,也可以作为图像识别的入门案例。目前通常使用如下几种方法:

方法名称 相关要点
tesseract 仅适合识别没有干扰和扭曲的图片,训练起来很麻烦
其他开源识别库 不够通用,识别率未知
付费OCR API 需求量大的情形成本很高
图像处理+机器学习分类算法 涉及多种技术,学习成本高,且不通用
卷积神经网络 一定的学习成本,算法适用于多类验证码

这里说一下使用传统的图像处理和机器学习算法,涉及多种技术:

  1. 图像处理
  • 前处理(灰度化、二值化)
  • 图像分割
  • 裁剪(去边框)
  • 图像滤波、降噪
  • 去背景
  • 颜色分离
  • 旋转
  1. 机器学习
  • KNN
  • SVM

使用这类方法对使用者的要求较高,且由于图片的变化类型较多,处理的方法不够通用,经常花费很多时间去调整处理步骤和相关算法。
而使用卷积神经网络,只需要通过简单的前处理,就可以实现大部分静态字符型验证码的端到端识别,效果很好,通用性很高。

这里列出目前常用的验证码生成库:

参考:Java验证全家桶

语言 验证码库名称 链接 样例
Java JCaptcha 示例 效果1 效果2 效果3
Java JCaptcha4Struts2
Java SimpleCaptcha 例子 效果1 效果2 效果3
Java kaptcha 例子 水纹效果 鱼眼效果 阴影效果
Java patchca 效果1
Java imageRandom
Java iCaptcha 效果1
Java SkewPassImage 效果1
Java Cage 效果1 效果2
Python captcha 例子 py_Captcha
Python pycapt 例子 pycapt
PHP Gregwar/Captcha 文档
PHP mewebstudio/captcha 文档

1.2 目录结构

1.2.1 基本配置

序号 文件名称 说明
1 conf/ 配置文件目录
2 sample/ 数据集目录
3 model/ 模型文件目录
4 cnnlib/ 封装CNN的相关代码目录

1.2.2 训练模型

序号 文件名称 说明
1 verify_and_split_data.py 验证数据集、拆分数据为训练集和测试集
2 network.py cnn网络基类
3 train_model.py 训练模型
4 test_batch.py 批量验证
5 gen_image/gen_sample_by_captcha.py 生成验证码的脚本
6 gen_image/collect_labels.py 用于统计验证码标签(常用于中文验证码)

1.2.3 web接口

序号 文件名称 说明
1 webserver_captcha_image.py 获取验证码接口
2 webserver_recognize_api.py 提供在线识别验证码接口
3 recognize_online.py 使用接口识别的例子
4 recognize_local.py 测试本地图片的例子
5 recognize_time_test.py 压力测试识别耗时和请求响应耗时

1.3 依赖

1
pip install -r requirements.txt

注意:如果需要使用GPU进行训练,请把文件中的tenforflow修改为tensorflow-gpu

1.4 模型结构

序号 层级
输入 input
1 卷积层 + 池化层 + 降采样层 + ReLU
2 卷积层 + 池化层 + 降采样层 + ReLU
3 卷积层 + 池化层 + 降采样层 + ReLU
4 全连接 + 降采样层 + Relu
5 全连接 + softmax
输出 output

2 如何使用

2.1 数据集

原始数据集可以存放在./sample/origin目录中。
为了便于处理,图片最好以2e8j_17322d3d4226f0b5c5a71d797d2ba7f7.jpg格式命名(标签_序列号.后缀)。

如果你没有训练集,你可以使用gen_sample_by_captcha.py文件生成训练集文件。
生成之前你需要修改相关配置conf/captcha_config.json(路径、文件后缀、字符集等)。

1
2
3
4
5
6
7
8
9
{
"root_dir": "sample/origin/", # 验证码保存路径
"image_suffix": "png", # 验证码图片后缀
"characters": "0123456789", # 生成验证码的可选字符
"count": 1000, # 生成验证码的图片数量
"char_count": 4, # 每张验证码图片上的字符数量
"width": 100, # 图片宽度
"height": 60 # 图片高度
}

2.2 配置文件

创建一个新项目前,需要自行修改相关配置文件conf/sample_config.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"origin_image_dir": "sample/origin/", # 原始文件
"new_image_dir": "sample/new_train/", # 新的训练样本
"train_image_dir": "sample/train/", # 训练集
"test_image_dir": "sample/test/", # 测试集
"api_image_dir": "sample/api/", # api接收的图片储存路径
"online_image_dir": "sample/online/", # 从验证码url获取的图片的储存路径
"local_image_dir": "sample/local/", # 本地保存图片的路径
"model_save_dir": "model/", # 从验证码url获取的图片的储存路径
"image_width": 100, # 图片宽度
"image_height": 60, # 图片高度
"max_captcha": 4, # 验证码字符个数
"image_suffix": "png", # 图片文件后缀
"char_set": "0123456789abcdefghijklmnopqrstuvwxyz", # 验证码识别结果类别
"use_labels_json_file": false, # 是否开启读取`labels.json`内容
"remote_url": "http://127.0.0.1:6100/captcha/", # 验证码远程获取地址
"cycle_stop": 3000, # 启动任务后的训练指定次数后停止
"acc_stop": 0.99, # 训练到指定准确率后停止
"cycle_save": 500, # 训练指定次数后定时保存模型
"enable_gpu": 0, # 是否开启GUP训练
"train_batch_size": 128, # 训练时每次使用的图片张数,如果CPU或者GPU内存太小可以减少这个参数
"test_batch_size": 100 # 每批次测试时验证的图片张数,不要超过验证码集的总数
}

关于验证码识别结果类别,假设你的样本是中文验证码,你可以使用tools/collect_labels.py脚本进行标签的统计。
会生成文件gen_image/labels.json存放所有标签,在配置文件中设置use_labels_json_file = True开启读取labels.json内容作为结果类别

2.3 验证和拆分数据集

此功能会校验原始图片集的尺寸和测试图片是否能打开,并按照19:1的比例拆分出训练集和测试集。
所以需要分别创建和指定三个文件夹:origin,train,test用于存放相关文件。

也可以修改为不同的目录,但是最好修改为绝对路径。
文件夹创建好之后,执行以下命令即可:

1
python3 verify_and_split_data.py

一般会有类似下面的提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> 开始校验目录:[sample/origin/]
开始校验原始图片集
原始集共有图片: 1001张
====以下1张图片有异常====
[第0张图片] [.DStore] [文件后缀不正确]
========end
开始分离原始图片集为:测试集(5%)和训练集(95%)
共分配1000张图片到训练集和测试集,其中1张为异常留在原始目录
测试集数量为:50
训练集数量为:950
>>> 开始校验目录:[sample/new_train/]
【警告】找不到目录sample/new_train/,即将创建
开始校验原始图片集
原始集共有图片: 0张
====以下0张图片有异常====
未发现异常(共 0 张图片)
========end
开始分离原始图片集为:测试集(5%)和训练集(95%)
共分配0张图片到训练集和测试集,其中0张为异常留在原始目录
测试集数量为:0
训练集数量为:0

程序会同时校验和分割origin_image_dirnew_image_dir两个目录中的图片;后续有了更多的样本,可以把样本放在new_image_dir目录中再次执行verify_and_split_data
程序会把无效的文件留在原文件夹。

此外,当你有新的样本需要一起训练,可以放在sample/new目录下,再次运行python3 verify_and_split_data.py即可。
需要注意的是,如果新的样本中有新增的标签,你需要把新的标签增加到char_set配置中或者labels.json文件中。

2.4 训练模型

创建好训练集和测试集之后,就可以开始训练模型了。
训练的过程中会输出日志,日志展示当前的训练轮数、准确率和loss。
此时的准确率是训练集图片的准确率,代表训练集的图片识别情况
例如:

1
2
3
第10次训练 >>> 
[训练集] 字符准确率为 0.03000 图片准确率为 0.00000 >>> loss 0.1698757857
[验证集] 字符准确率为 0.04000 图片准确率为 0.00000 >>> loss 0.1698757857

字符准确率和图片准确率的解释:

1
2
3
假设:有100张图片,每张图片四个字符,共400个字符。我们这里把任务拆分为为需要识别400个字符
字符准确率:识别400的字符中,正确字符的占比。
图片准确率:100张图片中,4个字符完全识别准确的图片占比。

这里不具体介绍tensorflow安装相关问题,直奔主题。
确保图片相关参数和目录设置正确后,执行以下命令开始训练:

1
python3 train_model.py

也可以根据train_model.pymain函数中的代码调用类开始训练或执行一次简单的识别演示。

由于训练集中常常不包含所有的样本特征,所以会出现训练集准确率是100%而测试集准确率不足100%的情况,此时提升准确率的一个解决方案是增加正确标记后的负样本。

2.5 批量验证

使用测试集的图片进行验证,输出准确率。

1
python3 test_batch.py

同样可以根据main函数中的代码调用类开始验证。

2.6 启动WebServer

项目已经封装好加载模型和识别图片的类,启动web server后调用接口就可以使用识别服务。
启动web server

1
python3 webserver_recognize_api.py

接口url为http://127.0.0.1:6000/b

2.7 调用接口识别

使用requests调用接口:

1
2
3
url = "http://127.0.0.1:6000/b"
files = {'image_file': (image_file_name, open('captcha.jpg', 'rb'), 'application')}
r = requests.post(url=url, files=files)

返回的结果是一个json:

1
2
3
4
{
'time': '1542017705.9152594',
'value': 'jsp1',
}

文件recognize_local.py是使用接口识别本地的例子,这个例子运行成功,那么识别验证码的一套流程基本上是走了一遍了。
在线识别验证码是显示中常用场景,文件recognize_online.py是使用接口在线识别的例子,参见:## 2.11 在线识别

2.8 部署

部署的时候,把webserver_recognize_api.py文件的最后一行修改为如下内容:

1
app.run(host='0.0.0.0',port=5000,debug=False)

然后开启端口访问权限,就可以通过外网访问了。
另外为了开启多进程处理请求,可以使用uwsgi+nginx组合进行部署。
这部分可以参考:Flask部署选择

2.9 部署多个模型

部署多个模型:
webserver_recognize_api.py文件汇总,新建一个Recognizer对象;
并参照原有up_image函数编写的路由和识别逻辑。

1
Q = Recognizer(image_height, image_width, max_captcha, char_set, model_save_dir)

注意修改这一行:

1
value = Q.rec_image(img)

2.10 在线识别

在线识别验证码是显示中常用场景,即实时获取目标验证码来调用接口进行识别。
为了测试的完整性,这里搭建了一个验证码获取接口,通过执行下面的命令启动:

1
python webserver_captcha_image.py

启动后通过访问此地址:http://127.0.0.1:6100/captcha/可以接收到验证码图片的二进制流文件。
具体进行在线识别任务的demo参见:recognize_online.py

3 数据统计

3.1 训练数据统计

由于很多同学提出,“需要训练多久呀?”、“准确率可以达到多少?”、“为什么我的准确率一直是0?”类似的疑问。
这一小节,使用默认配置(2019.06.02),把训练过程中的数据做了统计,给大家做一个展示。
本次测试条件如下:

  • 验证码:本项目自带生成验证码程序,数字+小写英文
  • 数量:20000张
  • 计算引擎:GPU
  • GPU型号:笔记本,GTX 950X 2G显卡

经过测试:
5000次,25分钟,训练集字符准确率84%,图片准确率51%;
9190次,46分钟,训练集字符准确率100%,图片准确率100%;
12000,60分钟,测试集的准确率基本上已经跑不动了。

使用test_batch.py测试,日志如下:

1
100个样本识别耗时6.513171672821045秒,准确率37.0%

有37%的准确率,可以说是识别成功的第一步了。

曲线图如下:
训练集-
train_acc

测试集-
test_acc

3.2 压力测试和统计数据

提供了一个简易的压力测试脚本,可以统计api运行过程中识别耗时和请求耗时的相关数据,不过图需要自己用Excel拉出来。
打开文件recognize_time_test.py,修改main函数下的test_file路径,这里会重复使用一张图片来访问是被接口。
最后数据会储存在test.csv文件中。
使用如下命令运行:

1
2
3
4
5
python3 recognize_time_test.py
----输出如下
2938,5150,13:30:25,总耗时:29ms,识别:15ms,请求:14ms
2939,5150,13:30:25,总耗时:41ms,识别:21ms,请求:20ms
2940,5150,13:30:25,总耗时:47ms,识别:16ms,请求:31ms

这里对一个模型进行了两万次测试后,一组数据test.csv。
把test.csv使用箱线图进行分析后可以看到:
压力测试结果

  • 单次请求API总耗时(平均值):27ms
  • 单次识别耗时(平均值):12ms
  • 每次请求耗时(平均值):15ms
    其中有:请求API总耗时 = 识别耗时 + 请求耗时

4 开发说明

  • 20190209
  1. 目前tensorboard展示支持的不是很好。
  • 20190601
  1. 最近比较忙,issue回的有点慢,请大家见谅
  2. dev分支开发到一半一直没时间弄,今天儿童节花了一下午时间更新了一下:)
  3. 感谢看到这里的你,谢谢你的支持

4 已知BUG

  1. 使用pycharm启动recognize_api.py文件报错

    1
    2
    3
    4
    5
    6
    2018-12-01 00:35:15.106333: W T:\src\github\tensorflow\tensorflow\core\framework\op_kernel.cc:1273] OP_REQUIRES failed at save_restore_tensor.cc:170 : Invalid argument: Unsuccessful TensorSliceReader constructor: Failed to get matching files on ./model/: Not found: FindFirstFile failed for: ./model : ϵͳ�Ҳ���ָ����·����
    ; No such process
    ......
    tensorflow.python.framework.errors_impl.InvalidArgumentError: Unsuccessful TensorSliceReader constructor: Failed to get matching files on ./model/: Not found: FindFirstFile failed for: ./model : ϵͳ\udcd5Ҳ\udcbb\udcb5\udcbdָ\udcb6\udca8\udcb5\udcc4·\udcbe\udcb6\udca1\udca3
    ; No such process
    [[Node: save/RestoreV2 = RestoreV2[dtypes=[DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT, DT_FLOAT], _device="/job:localhost/replica:0/task:0/device:CPU:0"](_arg_save/Const_0_0, save/RestoreV2/tensor_names, save/RestoreV2/shape_and_slices)]]

    由pycharm默认设置了工作空间,导致读取相对路径的model文件夹出错。
    解决办法:编辑运行配置,设置工作空间为项目目录即可。
    bug_api启动失败

  2. FileNotFoundError: [Errno 2] No such file or directory: ‘xxxxxx’
    目录下有文件夹不存在,在指定目录创建好文件夹即可。

  3. api程序在运行过程中内存越占越大
    结果查阅资料:链接
    在迭代循环时,不能再包含任何张量的计算表达式,否在会内存溢出。
    将张量的计算表达式放到init初始化执行后,识别速度得到极大的提升。

  4. 加载多个模型报错
    原因是两个Recognizer对象都使用了默认的Graph。
    解决办法是在创建对象的时候不使用默认Graph,新建graph,这样每个Recognizer都使用不同的graph,就不会冲突了。

  5. Flask程序用于生产
    可以参考官方文档:Flask的生产配置

  6. OOM happens

    1
    2
    Hint: If you want to see a list of allocated tensors when OOM happens,
    add report_tensor_allocations_upon_oom to RunOptions for current allocation info.

    尽可能关闭其他占用GPU或者CPU的任务,或者减小sample_config.json中的train_batch_size参数。


北森全自动打卡

声明

本篇文章为摘录自GitHub,自己修改了部分代码,仅作为参考,详见原文

⏰ 定时脚本

同时兼容定时和通知两种功能

  • 发送 “开启循环” 开启定时功能
  • 发送 “关闭循环” 关闭定时功能
  • 修正 “日志” 功能BUG,发送日志文件到邮箱中
  • 添加 “连接” 功能,实现远程连接电脑 AutoJs Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
/*
* @Author: George Huan
* @Date: 2020-08-03 09:30:30
* @LastEditTime: 2022-03-26 10:56:25
* @Description: DingDing-Automatic-Clock-in (Run on AutoJs)
* @URL: https://github.com/georgehuan1994/DingDing-Automatic-Clock-in
*/

const ACCOUNT = "15868141080"
const PASSWORD = ""

const QQ = "2757412961"
const EMAILL_ADDRESS = "2757412961@qq.com"
const SERVER_CHAN = "SCT171904TfSMLDvHz2oGuB8NU9NdMg1W0"
const PUSH_DEER = "PushDeer发送密钥"

const PUSH_METHOD = {QQ: 1, Email: 2, ServerChan: 3, PushDeer: 4}

// 默认通信方式:
// PUSH_METHOD.QQ -- QQ
// PUSH_METHOD.Email -- Email
// PUSH_METHOD.ServerChan -- Server酱
// PUSH_METHOD.PushDeer -- Push Deer
var DEFAULT_MESSAGE_DELIVER = PUSH_METHOD.QQ;

const PACKAGE_ID_QQ = "com.tencent.mobileqq" // QQ
// const PACKAGE_ID_DD = "com.alibaba.android.rimet" // 钉钉
const PACKAGE_ID_DD = "com.alibaba.android.rimet.zju" // 浙大钉
const PACKAGE_ID_XMSF = "com.xiaomi.xmsf" // 小米推送服务
const PACKAGE_ID_TASKER = "net.dinglisch.android.taskerm" // Tasker
const PACKAGE_ID_MAIL_163 = "com.netease.mail" // 网易邮箱大师
const PACKAGE_ID_MAIL_ANDROID = "com.android.email" // 系统内置邮箱
const PACKAGE_ID_PUSHDEER = "com.pushdeer.os" // Push Deer

const LOWER_BOUND = 1 * 60 * 1000 // 最小等待时间:1min
const UPPER_BOUND = 5 * 60 * 1000 // 最大等待时间:5min
const ONE_MIN = 1 * 60 * 1000 // 等待时间:1min
const FIF_MIN = 15 * 60 * 1000 // 等待时间:0.5h
const HALF_AN_HOUR = 30 * 60 * 1000 // 等待时间:0.5h
const AN_HOUR = 1 * 60 * 60 * 1000 // 等待时间:1h

// 执行时的屏幕亮度(0-255), 需要"修改系统设置"权限
const SCREEN_BRIGHTNESS = 20

// 是否过滤通知
const NOTIFICATIONS_FILTER = true

// PackageId白名单
const PACKAGE_ID_WHITE_LIST = [PACKAGE_ID_QQ,PACKAGE_ID_DD,PACKAGE_ID_XMSF,PACKAGE_ID_MAIL_163,PACKAGE_ID_TASKER,PACKAGE_ID_PUSHDEER]

// 公司的钉钉CorpId, 获取方法见 2020-09-24 更新日志。如果只加入了一家公司, 可以不填
// dingtalk://dingtalkclient/page/link?url='https://attend.dingtalk.com/attend/index.html?corpId=dingca07f56daee3def1bc961a6cb783455b'
const CORP_ID = "dingca07f56daee3def1bc961a6cb783455b"

// 锁屏意图, 配合 Tasker 完成锁屏动作, 具体配置方法见 2021-03-09 更新日志
const ACTION_LOCK_SCREEN = "autojs.intent.action.LOCK_SCREEN"

// 监听音量+键, 开启后无法通过音量+键调整音量, 按下音量+键:结束所有子线程
const OBSERVE_VOLUME_KEY = true

const WEEK_DAY = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday",]


// =================== ↓↓↓ 主线程:监听通知 ↓↓↓ ====================

var currentDate = new Date()

// 是否暂停循环打卡
var recursiveCard = false
var timeAnchorPoints = [8,11,13,17,19,22]

// 是否暂停定时打卡
var suspend = false

// 本次打开钉钉前是否需要等待
var needWaiting = false

// 运行日志路径
var globalLogFilePath = "/sdcard/脚本/Archive/" + getCurrentDate() + "-log.txt"

// 检查无障碍权限
auto.waitFor("normal")

// 检查Autojs版本
requiresAutojsVersion("4.1.0")

// 创建运行日志
console.setGlobalLogConfig({
file: "/sdcard/脚本/Archive/" + getCurrentDate() + "-log.txt"
});

// 监听本机通知
events.observeNotification()
events.on("notification", function(n) {
notificationHandler(n)
});

events.setKeyInterceptionEnabled("volume_up", OBSERVE_VOLUME_KEY)

if (OBSERVE_VOLUME_KEY) {
events.observeKey()
};

// 监听音量+键
events.onKeyDown("volume_up", function(event){
threads.shutDownAll()
device.setBrightnessMode(1)
device.cancelKeepingAwake()
toast("已中断所有子线程!")

// 可以在此调试各个方法
// doClock()
// sendQQMsg("测试文本")
// sendEmail("测试主题", "测试文本", null)
// sendServerChan(测试主题, 测试文本)
// sendPushDeer(测试主题, 测试文本)
});

toastLog("监听中, 请在日志中查看记录的通知及其内容")

// =================== ↑↑↑ 主线程:监听通知 ↑↑↑ =====================


// =================== ↑↑↑ 副线程:循环打卡 ↑↑↑ =====================
startRecursiveCard();

function startRecursiveCard(){
while(recursiveCard){
var hour = new Date().getHours()
// 直到打卡时间点 启动钉钉
while(recursiveCard && !existsInArray(timeAnchorPoints, hour)){
var randomTime = random(ONE_MIN, FIF_MIN)
console.log("【循环打卡】未到时间点,进行" + Math.floor(randomTime / 1000) + "秒随机睡眠...")
sleep(randomTime)
hour = new Date().getHours()
}

doClock(); // 打卡
console.log(new Date() + " 【循环打卡】打卡成功")
if(DEFAULT_MESSAGE_DELIVER == PUSH_METHOD.QQ){
sleep(10000) // 等待默认消息通知程序发送
sendQQMsg(new Date() + " 【循环打卡】打卡成功")
sendServerChan("【循环打卡】打卡结果", new Date() + " 打卡成功")
console.log("【循环打卡】QQ&ServerChan 消息发送成功...")
}

// 直到打卡时间点 启动钉钉
while(recursiveCard && existsInArray(timeAnchorPoints, hour)){
var randomTime = random(ONE_MIN, FIF_MIN)
console.log("【循环打卡】已打卡,进行" + Math.floor(randomTime / 1000) + "秒随机睡眠...")
sleep(randomTime)
hour = new Date().getHours()
}
};
}
// =================== ↑↑↑ 副线程:循环打卡 ↑↑↑ =====================

/**
* @description 处理通知
*/
function notificationHandler(n) {

var packageId = n.getPackageName() // 获取通知包名
var abstract = n.tickerText // 获取通知摘要
var text = n.getText() // 获取通知文本

// 过滤 PackageId 白名单之外的应用所发出的通知
if (!filterNotification(packageId, abstract, text)) {
return;
}

// 监听摘要为 "定时打卡" 的通知, 不一定要从 Tasker 中发出通知, 日历、定时器等App均可实现
if (abstract == "定时打卡" && !suspend) {
needWaiting = true
threads.shutDownAll()
threads.start(function(){
doClock()
})
return;
}

switch(text) {

case "打卡": // 监听文本为 "打卡" 的通知
needWaiting = false
// threads.shutDownAll()
threads.start(function(){
doClock()
})
break;

case "查询": // 监听文本为 "查询" 的通知
// threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg(getStorageData("dingding", "clockResult"))
break;
case PUSH_METHOD.Email:
sendEmail("考勤结果", getStorageData("dingding", "clockResult"), null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("考勤结果", getStorageData("dingding", "clockResult"))
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("考勤结果", getStorageData("dingding", "clockResult"))
break;
}
})
break;

case "暂停": // 监听文本为 "暂停" 的通知
suspend = true
console.warn("暂停定时打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("修改成功, 已暂停定时打卡功能")
break;
case PUSH_METHOD.Email:
sendEmail("修改成功", "已暂停定时打卡功能", null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("修改成功", "已暂停定时打卡功能")
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("修改成功", "已暂停定时打卡功能")
break;
}
})
break;

case "恢复": // 监听文本为 "恢复" 的通知
suspend = false
console.warn("恢复定时打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("修改成功, 已恢复定时打卡功能")
break;
case PUSH_METHOD.Email:
sendEmail("修改成功", "已恢复定时打卡功能", null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("修改成功", "已恢复定时打卡功能")
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("修改成功", "已恢复定时打卡功能")
break;
}
})
break;

case "日志": // 监听文本为 "日志" 的通知
// threads.shutDownAll()
threads.start(function(){
sendEmail("获取日志", globalLogFilePath, globalLogFilePath)
})
break;


case "连接": // 连接到 Autojs Server
// threads.shutDownAll()
threads.start(function(){
connectServer()
})
break;

case "开启循环": // 每天循环打卡 早八晚十
recursiveCard = true
console.warn("每天循环打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("开启每天循环打卡功能(早八晚十),时间点:" + timeAnchorPoints)
break;
case PUSH_METHOD.Email:
sendEmail("开启每天循环打卡功能(早八晚十),时间点:" + timeAnchorPoints, null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("开启循环打卡成功", "开启每天循环打卡功能(早八晚十),时间点:" + timeAnchorPoints)
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("开启循环打卡成功", "开启每天循环打卡功能(早八晚十),时间点:" + timeAnchorPoints)
break;
}
startRecursiveCard();
})
break;

case "关闭循环": // 每天循环打卡 早八晚十
recursiveCard = false
console.warn("关闭每天循环打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("关闭每天循环打卡功能")
break;
case PUSH_METHOD.Email:
sendEmail("关闭每天循环打卡功能", null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("关闭循环打卡成功", "关闭每天循环打卡功能")
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("关闭循环打卡成功", "关闭每天循环打卡功能")
break;
}
})
break;

default:
break;
}

if (text == null)
return;

// 监听钉钉返回的考勤结果
// if (packageId == PACKAGE_ID_DD && text.indexOf("考勤打卡") >= 0) {
if (packageId == PACKAGE_ID_DD) {
setStorageData("dingding", "clockResult", text)
console.warn("监听钉钉返回的消息...")
// threads.shutDownAll()
threads.start(function() {
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg(text)
break;
case PUSH_METHOD.Email:
sendEmail("考勤结果", text, cameraFilePath)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("考勤结果", text)
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("考勤结果", text)
break;
}
})
return;
}
}


/**
* @description 打卡流程
*/
function doClock() {

currentDate = new Date()
console.log("本地时间: " + getCurrentDate() + " " + getCurrentTime())
console.log("开始打卡流程!")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕
holdOn() // 随机等待
signIn() // 自动登录
handleLate() // 处理迟到
attendKaoqin() // 考勤打卡

if (null != textContains("上班打卡").findOne(1000))
clockIn() // 上班打卡
else if (null != textContains("下班打卡").findOne(1000))
clockOut() // 下班打卡

lockScreen() // 关闭屏幕
}


/**
* @description 发送邮件流程
* @param {string} title 邮件主题
* @param {string} message 邮件正文
* @param {string} attachFilePath 要发送的附件路径
*/
function sendEmail(title, message, attachFilePath) {

console.log("开始发送邮件流程!")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕


// 内置电子邮件
app.launch("com.android.email");
console.log("等待邮箱启动...")
sleep(3000) // 等待邮箱启动

if(attachFilePath != null && files.exists(attachFilePath)) {
console.info(attachFilePath)
app.sendEmail({
email: [EMAILL_ADDRESS], subject: title, text: message, attachment: "file://" + attachFilePath
})
}
else {
console.error(attachFilePath)
app.sendEmail({
email: [EMAILL_ADDRESS], subject: title, text: message
})
}
sleep(1000)

// 选择邮箱应用,系统默认应用即可
var emailText = text("电子邮件").findOne(1000);
if(null == emailText){
console.log("邮箱应用选择失败...")
return;
}
emailText.parent().click();
console.log("邮箱应用选择成功...")
sleep(1000)

// 点击发送按钮
var sendBtn = desc("发送").findOne(1000);
if(null == sendBtn){
console.log("邮箱发送失败...")
return;
}
sendBtn.click()
console.log("正在发送邮件...")

// =============================================================================================================
// console.log("选择邮件应用")
// waitForActivity("com.android.internal.app.ChooserActivity") // 等待选择应用界面弹窗出现, 如果设置了默认应用就注释掉
// id("compose_send_btn").findOne().click()

// var emailAppName = app.getAppName(PACKAGE_ID_MAIL_163)
// if (null != emailAppName) {
// if (null != textMatches(emailAppName).findOne(1000)) {
// btn_email = textMatches(emailAppName).findOnce().parent()
// btn_email.click()
// }
// }
// else {
// console.error("不存在应用: " + PACKAGE_ID_MAIL_163)
// lockScreen()
// return;
// }

// // 网易邮箱大师
// var versoin = getPackageVersion(PACKAGE_ID_MAIL_163)
// console.log("应用版本: " + versoin)
// var sp = versoin.split(".")
// if (sp[0] == 6) {
// // 网易邮箱大师 6
// waitForActivity("com.netease.mobimail.activity.MailComposeActivity")
// id("send").findOne().click()
// }
// else {
// // 网易邮箱大师 7
// waitForActivity("com.netease.mobimail.module.mailcompose.MailComposeActivity")
// var input_address = id("input").findOne()
// if (null == input_address.getText()) {
// input_address.setText(EMAILL_ADDRESS)
// }
// id("iv_arrow").findOne().click()
// sleep(1000)
// id("img_send_bg").findOne().click()
// }


home()
sleep(2000)
lockScreen() // 关闭屏幕
}


/**
* @description 发送QQ消息
* @param {string} message 消息内容
*/
function sendQQMsg(message) {

console.log("发送QQ消息")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕

app.startActivity({
action: "android.intent.action.VIEW",
data:"mqq://im/chat?chat_type=wpa&version=1&src_type=web&uin=" + QQ,
packageName: "com.tencent.mobileqq",
});

// waitForActivity("com.tencent.mobileqq.activity.SplashActivity")

id("input").findOne().setText(message)
id("fun_btn").findOne().click()

home()
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description ServerChan推送
* @param {string} title 标题
* @param {string} message 消息
*/
function sendServerChan(title, message) {

console.log("向 ServerChan 发起推送请求")

url = "https://sctapi.ftqq.com/" + SERVER_CHAN + ".send";

res = http.post(encodeURI(url), {
"title": title,
"desp": message
});

console.log(res)
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description PushDeer推送
* @param {string} title 标题
* @param {string} message 消息
*/
function sendPushDeer(title, message) {

console.log("向 PushDeer 发起推送请求")

url = "https://api2.pushdeer.com/message/push"

res = http.post(encodeURI(url), {
"pushkey": PUSH_DEER,
"text": title,
"desp": message,
"type": "markdown",
});

console.log(res)
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description 唤醒设备
*/
function brightScreen() {

console.log("唤醒设备")

device.setBrightnessMode(0) // 手动亮度模式
device.setBrightness(SCREEN_BRIGHTNESS)
device.wakeUpIfNeeded() // 唤醒设备
device.keepScreenOn() // 保持亮屏
sleep(1000) // 等待屏幕亮起

if (!device.isScreenOn()) {
console.warn("设备未唤醒, 重试")
device.wakeUpIfNeeded()
brightScreen()
}
else {
console.info("设备已唤醒")
}
sleep(1000)
}


/**
* @description 解锁屏幕
*/
function unlockScreen() {

console.log("解锁屏幕")

if (isDeviceLocked()) {

gesture(
320, // 滑动时间:毫秒
[
device.width * 0.5, // 滑动起点 x 坐标:屏幕宽度的一半
device.height * 0.9 // 滑动起点 y 坐标:距离屏幕底部 10% 的位置, 华为系统需要往上一些
],
[
device.width / 2, // 滑动终点 x 坐标:屏幕宽度的一半
device.height * 0.1 // 滑动终点 y 坐标:距离屏幕顶部 10% 的位置
]
)

sleep(1000) // 等待解锁动画完成
home()
sleep(1000) // 等待返回动画完成
}

if (isDeviceLocked()) {
console.error("上滑解锁失败, 请按脚本中的注释调整 gesture(time, [x1,y1], [x2,y2]) 方法的参数!")
return;
}
console.info("屏幕已解锁")
}


/**
* @description 随机等待
*/
function holdOn(){

if (!needWaiting) {
return;
}

var randomTime = random(LOWER_BOUND, UPPER_BOUND)
toastLog(Math.floor(randomTime / 1000) + "秒后启动" + app.getAppName(PACKAGE_ID_DD) + "...")
sleep(randomTime)
}


/**
* @description 启动并登陆钉钉
*/
function signIn() {

app.launchPackage(PACKAGE_ID_DD)
console.log("正在启动" + app.getAppName(PACKAGE_ID_DD) + "...")

setVolume(0) // 设备静音

sleep(10000) // 等待钉钉启动

if (currentPackage() == PACKAGE_ID_DD &&
currentActivity() == "com.alibaba.android.user.login.SignUpWithPwdActivity") {
console.info("账号未登录")

var account = id("et_phone_input").findOne()
account.setText(ACCOUNT)
console.log("输入账号")

var password = id("et_pwd_login").findOne()
password.setText(PASSWORD)
console.log("输入密码")

var privacy = id("cb_privacy").findOne(1000)
if(null != privacy){
privacy.click()
console.log("同意隐私协议")
}

var btn_login = id("btn_next").findOne()
btn_login.click()
console.log("正在登陆...")

sleep(3000)
}

if (currentPackage() == PACKAGE_ID_DD &&
currentActivity() != "com.alibaba.android.user.login.SignUpWithPwdActivity") {
console.info("账号已登录")
sleep(1000)
}
}


/**
* @description 处理迟到打卡
*/
function handleLate(){

if (null != textMatches("迟到打卡").clickable(true).findOne(1000)) {
btn_late = textMatches("迟到打卡").clickable(true).findOnce()
btn_late.click()
console.warn("迟到打卡")
}
if (null != descMatches("迟到打卡").clickable(true).findOne(1000)) {
btn_late = descMatches("迟到打卡").clickable(true).findOnce()
btn_late.click()
console.warn("迟到打卡")
}
}


/**
* @description 使用 URL Scheme 进入考勤界面
*/
function attendKaoqin(){

var url_scheme = "dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html"

if(CORP_ID != "") {
url_scheme = url_scheme + "?corpId=" + CORP_ID
}

var a = app.intent({
// action: "VIEW",
action: "android.intent.action.VIEW",
data: url_scheme,
//flags: [Intent.FLAG_ACTIVITY_NEW_TASK]
});
app.startActivity(a);
console.log("正在进入考勤界面...")

textContains("申请").waitFor()
console.info("已进入考勤界面")
sleep(8000)
}


/**
* @description 上班打卡
*/
function clockIn() {

console.log("上班打卡...")

// if (null != textContains("已打卡").findOne(1000)) {
// console.info("已打卡")
// toast("已打卡")
// home()
// sleep(1000)
// return;
// }

console.log("等待连接到考勤机...")
sleep(2000)

if (null != textContains("未连接").findOne(1000)) {
console.error("未连接考勤机, 重新进入考勤界面!")
back()
sleep(2000)
attendKaoqin()
return;
}

textContains("已连接").waitFor()
console.info("已连接考勤机")
sleep(1000)

if (null != textMatches("上班打卡").clickable(true).findOne(1000)) {
btn_clockin = textMatches("上班打卡").clickable(true).findOnce()
btn_clockin.click()
console.log("按下打卡按钮")
}
else {
click(device.width / 2, device.height * 0.560)
console.log("点击打卡按钮坐标")
}
sleep(1000)
handleLate() // 处理迟到打卡

home()
sleep(1000)
}


/**
* @description 下班打卡
*/
function clockOut() {

console.log("下班打卡...")
console.log("等待连接到考勤机...")
sleep(2000)

if (null != textContains("未连接").findOne(1000)) {
console.error("未连接考勤机, 重新进入考勤界面!")
back()
sleep(2000)
attendKaoqin()
return;
}

textContains("已连接").waitFor()
console.info("已连接考勤机")
sleep(1000)

if (null != textMatches("下班打卡").clickable(true).findOne(1000)) {
btn_clockout = textMatches("下班打卡").clickable(true).findOnce()
btn_clockout.click()
console.log("按下打卡按钮")
sleep(1000)
}
else {
click(device.width / 2, device.height * 0.560)
console.log("点击打卡按钮坐标")
}

if (null != textContains("早退打卡").clickable(true).findOne(1000)) {
className("android.widget.Button").text("早退打卡").clickable(true).findOnce().parent().click()
console.warn("早退打卡")
}

home()
sleep(1000)
}


/**
* @description 锁屏
*/
function lockScreen(){

console.log("关闭屏幕")

// 锁屏方案1:Root
// Power()

// 锁屏方案2:No Root
// press(Math.floor(device.width / 2), Math.floor(device.height * 0.973), 1000) // 小米的快捷手势:长按Home键锁屏

// 万能锁屏方案:向Tasker发送广播, 触发系统锁屏动作。配置方法见 2021-03-09 更新日志
app.sendBroadcast({action: ACTION_LOCK_SCREEN});

device.setBrightnessMode(1) // 自动亮度模式
device.cancelKeepingAwake() // 取消设备常亮

if (isDeviceLocked()) {
console.info("屏幕已关闭")
}
else {
console.error("屏幕未关闭, 请尝试其他锁屏方案, 或等待屏幕自动关闭")
}
}


/**
* @description 连接 AutoJs Server
*/
function connectServer(){

console.log("开始连接 AutoJs Server!")
console.log("本地时间: " + getCurrentDate() + " " + getCurrentTime())

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕

connectAutoJsServer() // 连接 AutoJs Server

lockScreen() // 关闭屏幕

}

/**
*
* @description 启动 AutoJs Server 整体流程
*/
function connectAutoJsServer(){

launchApp("Auto.js")
console.log("正在启动 Auto.js...")
sleep(10000) // 等待 Auto.js 启动

// 打开侧拉菜单
var menuBtn = className("android.widget.ImageButton").desc("打开侧拉菜单").clickable(true).findOne(1000);
if(null != menuBtn){
menuBtn.click();
}
console.log("打开侧拉菜单...")
sleep(1000)

// 滚动屏幕找到连接电脑按钮
var menu = id("drawer_menu").findOne(1000);
if(null == menu){
console.log("滚动屏幕未找到连接电脑按钮...")
return;
}

menu.scrollForward()
console.log("滚动屏幕找到连接电脑按钮...")
sleep(1000)

// 连接电脑
var textCon = className("android.widget.TextView").text("连接电脑").findOne(1000);
if(null != textCon){
// 寻找按钮(Switch)
var layerCon = textCon.parent();
var switchBtn = layerCon.children().findOne(className("android.widget.Switch"));
console.log("找到连接电脑按钮,按钮状态为:" + switchBtn.text() + " " + switchBtn.checked() + "...")
if(switchBtn.checked() == false){
// 按下按钮
switchBtn.click();
console.log("开始连接电脑...")
sleep(2000)

// 点击确定
var okBtn = text("确定").findOne(1000);
okBtn.click();
console.log("连接电脑成功...")
}
}

sleep(1000) // 等待解锁动画完成
home()
sleep(1000) // 等待返回动画完成

// console.log("==========================================================");
// id("drawer_menu").findOne().children().forEach(child => {
// var target = child.findOne(id("sw"));
// console.log(target);
// });
}


// ===================== ↓↓↓ 功能函数 ↓↓↓ =======================

function dateDigitToString(num){
return num < 10 ? '0' + num : num
}

function getCurrentTime(){
var currentDate = new Date()
var hours = dateDigitToString(currentDate.getHours())
var minute = dateDigitToString(currentDate.getMinutes())
var second = dateDigitToString(currentDate.getSeconds())
var formattedTimeString = hours + ':' + minute + ':' + second
return formattedTimeString
}

function getCurrentDate(){
var currentDate = new Date()
var year = dateDigitToString(currentDate.getFullYear())
var month = dateDigitToString(currentDate.getMonth() + 1)
var date = dateDigitToString(currentDate.getDate())
var week = currentDate.getDay()
var formattedDateString = year + '-' + month + '-' + date + '-' + WEEK_DAY[week]
return formattedDateString
}

function existsInArray(arr, element){
for(var i=0; i<arr.length; i++){
if(arr[i] == element) return true;
}
return false;
}

// 通知过滤器
function filterNotification(bundleId, abstract, text) {
var check = PACKAGE_ID_WHITE_LIST.some(function(item) {return bundleId == item})
if (!NOTIFICATIONS_FILTER || check) {
console.verbose(bundleId)
console.verbose(abstract)
console.verbose(text)
console.verbose("---------------------------")
return true
}
else {
return false
}
}

// 保存本地数据
function setStorageData(name, key, value) {
const storage = storages.create(name) // 创建storage对象
storage.put(key, value)
}

// 读取本地数据
function getStorageData(name, key) {
const storage = storages.create(name)
if (storage.contains(key)) {
return storage.get(key, "")
}
// 默认返回undefined
}

// 删除本地数据
function delStorageData(name, key) {
const storage = storages.create(name)
if (storage.contains(key)) {
storage.remove(key)
}
}

// 获取应用版本号
function getPackageVersion(bundleId) {
importPackage(android.content)
var pckMan = context.getPackageManager()
var packageInfo = pckMan.getPackageInfo(bundleId, 0)
return packageInfo.versionName
}

// 屏幕是否为锁定状态
function isDeviceLocked() {
importClass(android.app.KeyguardManager)
importClass(android.content.Context)
var km = context.getSystemService(Context.KEYGUARD_SERVICE)
return km.isKeyguardLocked()
}

// 设置媒体和通知音量
function setVolume(volume) {
device.setMusicVolume(volume)
device.setNotificationVolume(volume)
console.verbose("媒体音量:" + device.getMusicVolume())
console.verbose("通知音量:" + device.getNotificationVolume())
}

Auto.js-VSCodeExt README

桌面编辑器Visual Studio Code的插件。可以让Visual Studio Code支持Auto.js开发。

Install

在VS Code中菜单”查看”->”扩展”->输入”Auto.js”或”hyb1996”搜索,即可看到”Auto.js-VSCodeExt”插件,安装即可。插件的更新也可以在这里更新。

Features

目前功能比较基础,仅支持:

  • 在VS Code的开发者工具实时显示Auto.js的日志与输出
  • 在VS Code命令中增加Run, Stop, Rerun, Stop all等选项。可以在手机与电脑连接后把Sublime编辑器中的脚本推送到AutoJs中执行,或者停止AutoJs中运行的脚本。

Usage

Step 1

Ctrl+Shift+P 或点击”查看”->”命令面板”可调出命令面板,输入 Auto.js 可以看到几个命令,移动光标到命令Auto.js: Start Server,按回车键执行该命令。

此时VS Code会在右上角显示”Auto.js server running”,即开启服务成功。

Step 2

将手机连接到电脑启用的Wifi或者同一局域网中。通过命令行ipconfig(或者其他操作系统的相同功能命令)查看电脑的IP地址。在Auto.js的侧拉菜单中启用调试服务,并输入IP地址,等待连接成功。

Step 3

之后就可以在电脑上编辑JavaScript文件并通过命令Run或者按键F5在手机上运行了。

Commands

Ctrl+Shift+P 或点击”查看”->”命令面板”可调出命令面板,输入 Auto.js 可以看到几个命令:

  • Start Server: 启动插件服务。之后在确保手机和电脑在同一区域网的情况下,在Auto.js的侧拉菜单中使用连接电脑功能连接。
  • Stop Server: 停止插件服务。
  • Run 运行当前编辑器的脚本。如果有多个设备连接,则在所有设备运行。
  • Rerun 停止当前文件对应的脚本并重新运行。如果有多个设备连接,则在所有设备重新运行。
  • Stop 停止当前文件对应的脚本。如果有多个设备连接,则在所有设备停止。
  • StopAll 停止所有正在运行的脚本。如果有多个设备连接,则在所有设备运行所有脚本。
  • Save 保存当前文件到手机的脚本默认目录(文件名会加上前缀remote)。如果有多个设备连接,则在所有设备保存。
  • RunOnDevice: 弹出设备菜单并在指定设备运行脚本。
  • SaveToDevice: 弹出设备菜单并在指定设备保存脚本。
  • New Project(新建项目):选择一个空文件夹(或者在文件管理器中新建一个空文件夹),将会自动创建一个项目
  • Run Project(运行项目):运行一个项目,需要Auto.js 4.0.4Alpha5以上支持
  • Save Project(保存项目):保存一个项目,需要Auto.js 4.0.4Alpha5以上支持

以上命令一些有对应的快捷键,参照命令后面的说明即可。

Log

要显示来自Auto.js的日志,打开 VS Code上面菜单的”帮助”->”切换开发人员工具”->”Console”即可。

钉钉打卡脚本原文

⏰ DingDing-Automatic-Clock-in

📖 简介

钉钉全自动打卡 + 远程打卡脚本,无需 root,基于 auto.js,适用于蓝牙考勤机。

💥 功能

  • 定时打卡
  • 远程打卡
  • 发送考勤结果

⚙️ 工具

  • auto.js
  • Tasker
  • 一款通讯应用(示例脚本中使用的是 QQ / 网易邮箱大师 / ServerChan / PushDeer,彼此互为备用方案)

💡 原理

通过 auto.js 脚本监听本机通知,在 Tasker 中创建定时任务,发出通知,或在另一设备上发送消息到本机,即可触发脚本中的打卡进程,实现定时打卡和远程打卡。

image

同理,监听到钉钉发出的打卡成功通知后,将通知文本通过 QQ消息 或 邮件正文 发送,实现发送考勤结果的功能。

📝 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
/*
* @Author: George Huan
* @Date: 2020-08-03 09:30:30
* @LastEditTime: 2022-03-26 10:56:25
* @Description: DingDing-Automatic-Clock-in (Run on AutoJs)
* @URL: https://github.com/georgehuan1994/DingDing-Automatic-Clock-in
*/

const ACCOUNT = "钉钉账号"
const PASSWORD = "钉钉密码"

const QQ = "用于接收打卡结果的QQ号"
const EMAILL_ADDRESS = "用于接收打卡结果的邮箱地址"
const SERVER_CHAN = "Server酱发送密钥"
const PUSH_DEER = "PushDeer发送密钥"

const PUSH_METHOD = {QQ: 1, Email: 2, ServerChan: 3, PushDeer: 4}

// 默认通信方式:
// PUSH_METHOD.QQ -- QQ
// PUSH_METHOD.Email -- Email
// PUSH_METHOD.ServerChan -- Server酱
// PUSH_METHOD.PushDeer -- Push Deer
var DEFAULT_MESSAGE_DELIVER = PUSH_METHOD.QQ;

const PACKAGE_ID_QQ = "com.tencent.mobileqq" // QQ
const PACKAGE_ID_DD = "com.alibaba.android.rimet" // 钉钉
const PACKAGE_ID_XMSF = "com.xiaomi.xmsf" // 小米推送服务
const PACKAGE_ID_TASKER = "net.dinglisch.android.taskerm" // Tasker
const PACKAGE_ID_MAIL_163 = "com.netease.mail" // 网易邮箱大师
const PACKAGE_ID_MAIL_ANDROID = "com.android.email" // 系统内置邮箱
const PACKAGE_ID_PUSHDEER = "com.pushdeer.os" // Push Deer

const LOWER_BOUND = 1 * 60 * 1000 // 最小等待时间:1min
const UPPER_BOUND = 5 * 60 * 1000 // 最大等待时间:5min

// 执行时的屏幕亮度(0-255), 需要"修改系统设置"权限
const SCREEN_BRIGHTNESS = 20

// 是否过滤通知
const NOTIFICATIONS_FILTER = true

// PackageId白名单
const PACKAGE_ID_WHITE_LIST = [PACKAGE_ID_QQ,PACKAGE_ID_DD,PACKAGE_ID_XMSF,PACKAGE_ID_MAIL_163,PACKAGE_ID_TASKER,PACKAGE_ID_PUSHDEER]

// 公司的钉钉CorpId, 获取方法见 2020-09-24 更新日志。如果只加入了一家公司, 可以不填
const CORP_ID = ""

// 锁屏意图, 配合 Tasker 完成锁屏动作, 具体配置方法见 2021-03-09 更新日志
const ACTION_LOCK_SCREEN = "autojs.intent.action.LOCK_SCREEN"

// 监听音量+键, 开启后无法通过音量+键调整音量, 按下音量+键:结束所有子线程
const OBSERVE_VOLUME_KEY = true

const WEEK_DAY = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday",]


// =================== ↓↓↓ 主线程:监听通知 ↓↓↓ ====================

var currentDate = new Date()

// 是否暂停定时打卡
var suspend = false

// 本次打开钉钉前是否需要等待
var needWaiting = true

// 运行日志路径
var globalLogFilePath = "/sdcard/脚本/Archive/" + getCurrentDate() + "-log.txt"

// 检查无障碍权限
auto.waitFor("normal")

// 检查Autojs版本
requiresAutojsVersion("4.1.0")

// 创建运行日志
console.setGlobalLogConfig({
file: "/sdcard/脚本/Archive/" + getCurrentDate() + "-log.txt"
});

// 监听本机通知
events.observeNotification()
events.on("notification", function(n) {
notificationHandler(n)
});

events.setKeyInterceptionEnabled("volume_up", OBSERVE_VOLUME_KEY)

if (OBSERVE_VOLUME_KEY) {
events.observeKey()
};

// 监听音量+键
events.onKeyDown("volume_up", function(event){
threads.shutDownAll()
device.setBrightnessMode(1)
device.cancelKeepingAwake()
toast("已中断所有子线程!")

// 可以在此调试各个方法
// doClock()
// sendQQMsg("测试文本")
// sendEmail("测试主题", "测试文本", null)
// sendServerChan(测试主题, 测试文本)
// sendPushDeer(测试主题, 测试文本)
});

toastLog("监听中, 请在日志中查看记录的通知及其内容")

// =================== ↑↑↑ 主线程:监听通知 ↑↑↑ =====================



/**
* @description 处理通知
*/
function notificationHandler(n) {

var packageId = n.getPackageName() // 获取通知包名
var abstract = n.tickerText // 获取通知摘要
var text = n.getText() // 获取通知文本

// 过滤 PackageId 白名单之外的应用所发出的通知
if (!filterNotification(packageId, abstract, text)) {
return;
}

// 监听摘要为 "定时打卡" 的通知, 不一定要从 Tasker 中发出通知, 日历、定时器等App均可实现
if (abstract == "定时打卡" && !suspend) {
needWaiting = true
threads.shutDownAll()
threads.start(function(){
doClock()
})
return;
}

switch(text) {

case "打卡": // 监听文本为 "打卡" 的通知
needWaiting = false
threads.shutDownAll()
threads.start(function(){
doClock()
})
break;

case "查询": // 监听文本为 "查询" 的通知
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg(getStorageData("dingding", "clockResult"))
break;
case PUSH_METHOD.Email:
sendEmail("考勤结果", getStorageData("dingding", "clockResult"), null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("考勤结果", getStorageData("dingding", "clockResult"))
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("考勤结果", getStorageData("dingding", "clockResult"))
break;
}
})
break;

case "暂停": // 监听文本为 "暂停" 的通知
suspend = true
console.warn("暂停定时打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("修改成功, 已暂停定时打卡功能")
break;
case PUSH_METHOD.Email:
sendEmail("修改成功", "已暂停定时打卡功能", null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("修改成功", "已暂停定时打卡功能")
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("修改成功", "已暂停定时打卡功能")
break;
}
})
break;

case "恢复": // 监听文本为 "恢复" 的通知
suspend = false
console.warn("恢复定时打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("修改成功, 已恢复定时打卡功能")
break;
case PUSH_METHOD.Email:
sendEmail("修改成功", "已恢复定时打卡功能", null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("修改成功", "已恢复定时打卡功能")
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("修改成功", "已恢复定时打卡功能")
break;
}
})
break;

case "日志": // 监听文本为 "日志" 的通知
threads.shutDownAll()
threads.start(function(){
sendEmail("获取日志", globalLogFilePath, globalLogFilePath)
})
break;

default:
break;
}

if (text == null)
return;

// 监听钉钉返回的考勤结果
if (packageId == PACKAGE_ID_DD && text.indexOf("考勤打卡") >= 0) {
setStorageData("dingding", "clockResult", text)
threads.shutDownAll()
threads.start(function() {
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg(text)
break;
case PUSH_METHOD.Email:
sendEmail("考勤结果", text, cameraFilePath)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("考勤结果", text)
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("考勤结果", text)
break;
}
})
return;
}
}


/**
* @description 打卡流程
*/
function doClock() {

currentDate = new Date()
console.log("本地时间: " + getCurrentDate() + " " + getCurrentTime())
console.log("开始打卡流程!")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕
holdOn() // 随机等待
signIn() // 自动登录
handleLate() // 处理迟到
attendKaoqin() // 考勤打卡

if (currentDate.getHours() <= 12)
clockIn() // 上班打卡
else
clockOut() // 下班打卡

lockScreen() // 关闭屏幕
}


/**
* @description 发送邮件流程
* @param {string} title 邮件主题
* @param {string} message 邮件正文
* @param {string} attachFilePath 要发送的附件路径
*/
function sendEmail(title, message, attachFilePath) {

console.log("开始发送邮件流程!")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕

if(attachFilePath != null && files.exists(attachFilePath)) {
console.info(attachFilePath)
app.sendEmail({
email: [EMAILL_ADDRESS], subject: title, text: message, attachment: attachFilePath
})
}
else {
console.error(attachFilePath)
app.sendEmail({
email: [EMAILL_ADDRESS], subject: title, text: message
})
}

console.log("选择邮件应用")
waitForActivity("com.android.internal.app.ChooserActivity") // 等待选择应用界面弹窗出现, 如果设置了默认应用就注释掉

var emailAppName = app.getAppName(PACKAGE_ID_MAIL_163)
if (null != emailAppName) {
if (null != textMatches(emailAppName).findOne(1000)) {
btn_email = textMatches(emailAppName).findOnce().parent()
btn_email.click()
}
}
else {
console.error("不存在应用: " + PACKAGE_ID_MAIL_163)
lockScreen()
return;
}

// 网易邮箱大师
var versoin = getPackageVersion(PACKAGE_ID_MAIL_163)
console.log("应用版本: " + versoin)
var sp = versoin.split(".")
if (sp[0] == 6) {
// 网易邮箱大师 6
waitForActivity("com.netease.mobimail.activity.MailComposeActivity")
id("send").findOne().click()
}
else {
// 网易邮箱大师 7
waitForActivity("com.netease.mobimail.module.mailcompose.MailComposeActivity")
var input_address = id("input").findOne()
if (null == input_address.getText()) {
input_address.setText(EMAILL_ADDRESS)
}
id("iv_arrow").findOne().click()
sleep(1000)
id("img_send_bg").findOne().click()
}

// 内置电子邮件
// waitForActivity("com.kingsoft.mail.compose.ComposeActivity")
// id("compose_send_btn").findOne().click()

console.log("正在发送邮件...")

home()
sleep(2000)
lockScreen() // 关闭屏幕
}


/**
* @description 发送QQ消息
* @param {string} message 消息内容
*/
function sendQQMsg(message) {

console.log("发送QQ消息")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕

app.startActivity({
action: "android.intent.action.VIEW",
data:"mqq://im/chat?chat_type=wpa&version=1&src_type=web&uin=" + QQ,
packageName: "com.tencent.mobileqq",
});

// waitForActivity("com.tencent.mobileqq.activity.SplashActivity")

id("input").findOne().setText(message)
id("fun_btn").findOne().click()

home()
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description ServerChan推送
* @param {string} title 标题
* @param {string} message 消息
*/
function sendServerChan(title, message) {

console.log("向 ServerChan 发起推送请求")

url = "https://sctapi.ftqq.com/" + SERVER_CHAN + ".send";

res = http.post(encodeURI(url), {
"title": title,
"desp": message
});

console.log(res)
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description PushDeer推送
* @param {string} title 标题
* @param {string} message 消息
*/
function sendPushDeer(title, message) {

console.log("向 PushDeer 发起推送请求")

url = "https://api2.pushdeer.com/message/push"

res = http.post(encodeURI(url), {
"pushkey": PUSH_DEER,
"text": title,
"desp": message,
"type": "markdown",
});

console.log(res)
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description 唤醒设备
*/
function brightScreen() {

console.log("唤醒设备")

device.setBrightnessMode(0) // 手动亮度模式
device.setBrightness(SCREEN_BRIGHTNESS)
device.wakeUpIfNeeded() // 唤醒设备
device.keepScreenOn() // 保持亮屏
sleep(1000) // 等待屏幕亮起

if (!device.isScreenOn()) {
console.warn("设备未唤醒, 重试")
device.wakeUpIfNeeded()
brightScreen()
}
else {
console.info("设备已唤醒")
}
sleep(1000)
}


/**
* @description 解锁屏幕
*/
function unlockScreen() {

console.log("解锁屏幕")

if (isDeviceLocked()) {

gesture(
320, // 滑动时间:毫秒
[
device.width * 0.5, // 滑动起点 x 坐标:屏幕宽度的一半
device.height * 0.9 // 滑动起点 y 坐标:距离屏幕底部 10% 的位置, 华为系统需要往上一些
],
[
device.width / 2, // 滑动终点 x 坐标:屏幕宽度的一半
device.height * 0.1 // 滑动终点 y 坐标:距离屏幕顶部 10% 的位置
]
)

sleep(1000) // 等待解锁动画完成
home()
sleep(1000) // 等待返回动画完成
}

if (isDeviceLocked()) {
console.error("上滑解锁失败, 请按脚本中的注释调整 gesture(time, [x1,y1], [x2,y2]) 方法的参数!")
return;
}
console.info("屏幕已解锁")
}


/**
* @description 随机等待
*/
function holdOn(){

if (!needWaiting) {
return;
}

var randomTime = random(LOWER_BOUND, UPPER_BOUND)
toastLog(Math.floor(randomTime / 1000) + "秒后启动" + app.getAppName(PACKAGE_ID_DD) + "...")
sleep(randomTime)
}


/**
* @description 启动并登陆钉钉
*/
function signIn() {

app.launchPackage(PACKAGE_ID_DD)
console.log("正在启动" + app.getAppName(PACKAGE_ID_DD) + "...")

setVolume(0) // 设备静音

sleep(10000) // 等待钉钉启动

if (currentPackage() == PACKAGE_ID_DD &&
currentActivity() == "com.alibaba.android.user.login.SignUpWithPwdActivity") {
console.info("账号未登录")

var account = id("et_phone_input").findOne()
account.setText(ACCOUNT)
console.log("输入账号")

var password = id("et_pwd_login").findOne()
password.setText(PASSWORD)
console.log("输入密码")

var privacy = id("cb_privacy").findOne()
privacy.click()
console.log("同意隐私协议")

var btn_login = id("btn_next").findOne()
btn_login.click()
console.log("正在登陆...")

sleep(3000)
}

if (currentPackage() == PACKAGE_ID_DD &&
currentActivity() != "com.alibaba.android.user.login.SignUpWithPwdActivity") {
console.info("账号已登录")
sleep(1000)
}
}


/**
* @description 处理迟到打卡
*/
function handleLate(){

if (null != textMatches("迟到打卡").clickable(true).findOne(1000)) {
btn_late = textMatches("迟到打卡").clickable(true).findOnce()
btn_late.click()
console.warn("迟到打卡")
}
if (null != descMatches("迟到打卡").clickable(true).findOne(1000)) {
btn_late = descMatches("迟到打卡").clickable(true).findOnce()
btn_late.click()
console.warn("迟到打卡")
}
}


/**
* @description 使用 URL Scheme 进入考勤界面
*/
function attendKaoqin(){

var url_scheme = "dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html"

if(CORP_ID != "") {
url_scheme = url_scheme + "?corpId=" + CORP_ID
}

var a = app.intent({
action: "VIEW",
data: url_scheme,
//flags: [Intent.FLAG_ACTIVITY_NEW_TASK]
});
app.startActivity(a);
console.log("正在进入考勤界面...")

textContains("申请").waitFor()
console.info("已进入考勤界面")
sleep(1000)
}


/**
* @description 上班打卡
*/
function clockIn() {

console.log("上班打卡...")

if (null != textContains("已打卡").findOne(1000)) {
console.info("已打卡")
toast("已打卡")
home()
sleep(1000)
return;
}

console.log("等待连接到考勤机...")
sleep(2000)

if (null != textContains("未连接").findOne(1000)) {
console.error("未连接考勤机, 重新进入考勤界面!")
back()
sleep(2000)
attendKaoqin()
return;
}

textContains("已连接").waitFor()
console.info("已连接考勤机")
sleep(1000)

if (null != textMatches("上班打卡").clickable(true).findOne(1000)) {
btn_clockin = textMatches("上班打卡").clickable(true).findOnce()
btn_clockin.click()
console.log("按下打卡按钮")
}
else {
click(device.width / 2, device.height * 0.560)
console.log("点击打卡按钮坐标")
}
sleep(1000)
handleLate() // 处理迟到打卡

home()
sleep(1000)
}


/**
* @description 下班打卡
*/
function clockOut() {

console.log("下班打卡...")
console.log("等待连接到考勤机...")
sleep(2000)

if (null != textContains("未连接").findOne(1000)) {
console.error("未连接考勤机, 重新进入考勤界面!")
back()
sleep(2000)
attendKaoqin()
return;
}

textContains("已连接").waitFor()
console.info("已连接考勤机")
sleep(1000)

if (null != textMatches("下班打卡").clickable(true).findOne(1000)) {
btn_clockout = textMatches("下班打卡").clickable(true).findOnce()
btn_clockout.click()
console.log("按下打卡按钮")
sleep(1000)
}
else {
click(device.width / 2, device.height * 0.560)
console.log("点击打卡按钮坐标")
}

if (null != textContains("早退打卡").clickable(true).findOne(1000)) {
className("android.widget.Button").text("早退打卡").clickable(true).findOnce().parent().click()
console.warn("早退打卡")
}

home()
sleep(1000)
}


/**
* @description 锁屏
*/
function lockScreen(){

console.log("关闭屏幕")

// 锁屏方案1:Root
// Power()

// 锁屏方案2:No Root
// press(Math.floor(device.width / 2), Math.floor(device.height * 0.973), 1000) // 小米的快捷手势:长按Home键锁屏

// 万能锁屏方案:向Tasker发送广播, 触发系统锁屏动作。配置方法见 2021-03-09 更新日志
app.sendBroadcast({action: ACTION_LOCK_SCREEN});

device.setBrightnessMode(1) // 自动亮度模式
device.cancelKeepingAwake() // 取消设备常亮

if (isDeviceLocked()) {
console.info("屏幕已关闭")
}
else {
console.error("屏幕未关闭, 请尝试其他锁屏方案, 或等待屏幕自动关闭")
}
}



// ===================== ↓↓↓ 功能函数 ↓↓↓ =======================

function dateDigitToString(num){
return num < 10 ? '0' + num : num
}

function getCurrentTime(){
var currentDate = new Date()
var hours = dateDigitToString(currentDate.getHours())
var minute = dateDigitToString(currentDate.getMinutes())
var second = dateDigitToString(currentDate.getSeconds())
var formattedTimeString = hours + ':' + minute + ':' + second
return formattedTimeString
}

function getCurrentDate(){
var currentDate = new Date()
var year = dateDigitToString(currentDate.getFullYear())
var month = dateDigitToString(currentDate.getMonth() + 1)
var date = dateDigitToString(currentDate.getDate())
var week = currentDate.getDay()
var formattedDateString = year + '-' + month + '-' + date + '-' + WEEK_DAY[week]
return formattedDateString
}

// 通知过滤器
function filterNotification(bundleId, abstract, text) {
var check = PACKAGE_ID_WHITE_LIST.some(function(item) {return bundleId == item})
if (!NOTIFICATIONS_FILTER || check) {
console.verbose(bundleId)
console.verbose(abstract)
console.verbose(text)
console.verbose("---------------------------")
return true
}
else {
return false
}
}

// 保存本地数据
function setStorageData(name, key, value) {
const storage = storages.create(name) // 创建storage对象
storage.put(key, value)
}

// 读取本地数据
function getStorageData(name, key) {
const storage = storages.create(name)
if (storage.contains(key)) {
return storage.get(key, "")
}
// 默认返回undefined
}

// 删除本地数据
function delStorageData(name, key) {
const storage = storages.create(name)
if (storage.contains(key)) {
storage.remove(key)
}
}

// 获取应用版本号
function getPackageVersion(bundleId) {
importPackage(android.content)
var pckMan = context.getPackageManager()
var packageInfo = pckMan.getPackageInfo(bundleId, 0)
return packageInfo.versionName
}

// 屏幕是否为锁定状态
function isDeviceLocked() {
importClass(android.app.KeyguardManager)
importClass(android.content.Context)
var km = context.getSystemService(Context.KEYGUARD_SERVICE)
return km.isKeyguardLocked()
}

// 设置媒体和通知音量
function setVolume(volume) {
device.setMusicVolume(volume)
device.setNotificationVolume(volume)
console.verbose("媒体音量:" + device.getMusicVolume())
console.verbose("通知音量:" + device.getNotificationVolume())
}

📐 工具介绍

Auto.js

Auto.js 是利用安卓系统的 「无障碍服务」 实现类似于按键精灵一样,可以通过代码模拟一系列界面动作的辅助工具。

与 「按键精灵」 不同的是,它的模拟动作并不是简单的使用在界面定坐标点来实现,而是找控件来实现的。

免费版:Auto.js 4.1.1a Alpha2-armeabi-v7a-release

github:GitHub - hyb1996/Auto.js

官方文档:首页 - Auto.js

推荐使用VS Code 插件进行调试,调试完成后,还能通过此插件将脚本保存到手机上。

Tasker

Tasker 也是一个安卓自动化神器,与 Auto.js 结合使用可胜任日常工作流。

此处仅提供 Tasker 5.0 及以下的官方原版,原版不含正版验证,使用不受限制:

Tasker.4.9u4m.apk

Tasker.5.0u7m.apk

Tasker 定时打卡配置:

  1. 添加一个 「通知」 操作任务,通知标题修改为 「定时打卡」,通知文字随意,通知优先级设为 1。
  2. 添加两个配置文件,使用日期和时间作为条件,分别在上班前和下班后触发。

你也可以下载配置文件,导入到 Tasker 中使用,方法如下:

  1. 长按 菜单栏-任务,导入”发送通知.tsk.xml”。
  2. 长按 菜单栏-配置文件,导入”上班打卡.prf.xml” 和 “下班打卡.prf.xml”。
  3. 在任务编辑界面左下方有一个三角形的播放按钮,点击即可发送通知,方便调试。

🕹️ 使用方法

远程打卡

  • 向本机的 QQ 发送消息 「打卡」,或回复标题为 「打卡」 的邮件,或向 PushDeer 发送标题为「打卡」 的推送请求,即可触发打卡进程。
  • 向本机的 QQ 发送消息 「查询」,或回复标题为 「查询」 的邮件,或向 PushDeer 发送标题为「查询」 的推送请求,即可查询最新一次打卡结果。

暂停/恢复定时打卡

  • 向本机的 QQ 发送消息 「暂停」,或回复标题为 「暂停」 的邮件,或向 PushDeer 发送标题为「暂停」 的推送请求,即可暂停定时打卡功能(仅暂停定时打卡,不影响远程打卡功能)
  • 向本机的 QQ 发送消息 「恢复」,或回复标题为 「恢复」 的邮件,或向 PushDeer 发送标题为「恢复」 的推送请求,即可恢复定时打卡功能。

⚠️ 注意事项 (必读!!!)

  • AutoJs Pro 版本屏蔽了一些主流应用,如果要使用 QQ 作为回复方式,不要使用 AutoJs Pro 版!
  • 首次启动 AutoJs,需要为其开启无障碍权限。
  • 运行脚本前,请在 AutoJs 菜单栏中(从屏幕左边划出),开启 「通知读取权限」。
  • 若无法通过 app.launchPackage() 方法启动应用,请开启该应用的「自启动」「允许后台弹窗」。
  • AutoJs、Tasker 可息屏运行,需要在系统设置中开启通知亮屏。
  • 为保证 AutoJs、Tasker 进程不被系统清理,可调整它们的电池管理策略、加入管理应用的白名单,为其开启前台服务、添加应用锁…
  • 虽然脚本可执行完整的打卡步骤,但推荐开启钉钉的极速打卡功能,在钉钉启动时即可完成打卡,应把后续的步骤视为极速打卡失败后的保险措施。

📜 更新日志

2022-03-26

  1. 可以通过 PushDeer 接收通知、推送考勤结果

2022-03-01

  1. 可以通过Server酱来推送考勤结果

2021-10-23

  1. 适配网易邮箱大师7.0

2021-09-02

  1. 新增获取日志功能,发送 「日志」,可将运行日志作为邮件附件发送(最好使用内置邮件)
  2. 优化通知过滤器,过滤 Tasker 发出的无效通知

2021-07-07

  1. 登录流程自动同意隐私协议

2021-05-27

  1. 修改了部分常量的命名
  2. 移除了休息日不打卡的判断
  3. 在邮件的基础上,增加QQ作为新的通讯方式。除发送考勤结果需要手动指定应用外,使用QQ向本机发送 「查询、暂停、恢复」 指令,则会用QQ来回复查询或操作结果;使用邮件向本机发送指令则用邮件回复。

2021-05-06

  1. 增加音量上键监听,按下后中断所有子线程,也可以利用回调来进行调试
  2. 不再使用考勤机名称来判断连接状态
  3. 重新进入打卡界面前,先返回上级菜单,以解决顶号登录无法正常连接到考勤机的问题
  4. 启动钉钉时,将媒体音量和通知音量设为0

2021-03-15

  1. 运行时检查Auto.js版本,脚本需要在Auto.js 4.1.0及以上版本中运行
  2. 新增解锁是否成功的判断,若解锁失败则停止运行脚本
  3. 优化 signIn() 方法,使用 bundleId + activity 来判断登录情况
  4. 优化部分控件和信息的获取方式

2021-03-09

  1. 移除 「结束钉钉」、「检查更新」 这个两个过程,使用最近一次监测到的正在运行的应用的包名进行判断

  2. 补充一个万能锁屏方案:向Tasker发送广播,触发Tasker中的系统锁屏操作。

    • 在Tasker中添加一个任务,在任务中添加操作 「系统锁屏(关闭屏幕)」
    • 在Tasker中添加一个事件类型的配置文件,事件类别:系统-收到的意图
    • 在事件操作中填写:autojs.intent.action.LOCK_SCREEN ,保持发送方与接收方的action一致即可
1
2
3
app.sendBroadcast({
action: 'autojs.intent.action.LOCK_SCREEN'
});

2021-02-07

  1. 防止监听事件被耗时操作阻塞。

2021-01-15

  1. 移除 「进入工作台」 以及 「进入考勤打卡界面」 这两个过程
  2. 启动并成功登录钉钉后,直接使用intent拉起考勤打卡界面

2021-01-08

  1. 修复:通知过滤器报错

2020-12-30

  1. 优化:现在可以通过邮件来 暂停/恢复 定时打卡功能,以应对停工停产,或其他需要暂时停止定时打卡的特殊情况

2020-12-04

  1. 优化:打卡过程在子线程中执行,钉钉返回打卡结果后,直接中断子线程,减少无效操作

2020-10-27

  1. 修复:当钉钉的通知文本为null时,indexOf()方法无法正常执行

2020-09-24

  1. 优化:使用URL Scheme直接拉起考勤打卡界面
1
2
3
4
5
6
7
8
function attendKaoqin(){
var a = app.intent({
action: "VIEW",
data: "dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html"
});
app.startActivity(a);
sleep(5000)
}

获取URL的方式如下:

  1. 在PC端找到 「智能工作助理」 联系人
  2. 发送消息 “打卡” ,点击 「立即打卡」
  3. 弹出一个二维码。此二维码就是拉起考勤打卡界面的 URL,用自带的相机或其他应用扫描,并在浏览器中打开,即可获得完整URL
  4. 观察获取到的URL,找到 CorpId=xxxxxxxxxxxxxxxxxxx ,将CorpId的值填写到的脚本开头的CORP_ID这个常量中
  5. 仅使用 dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html,也可以拉起旧版打卡界面,钉钉会自动获取企业的CorpId。如果加入了多个组织,且没有填写CorpId,则在拉起考勤界面时会弹出一个选择组织的对话框。

2020-09-11

  1. 将上次考勤结果储存在本地
  2. 将运行日志储存在本地 /sdcard/脚本/Archive/
  3. 修复在下班极速打卡之后,重复打卡的问题

2020-09-04

  1. 将 “打卡” 与 “发送邮件” 分离成两个过程,打卡完成后,将钉钉返回的考勤结果作为邮件正文发送

2020-09-02

  1. 改为使用 “去打卡” 文本获取按钮。若找不到 “去打卡” 按钮,则直接点击 “考勤打卡” 的屏幕坐标

📢 声明

此仓库及脚本仅供学习交流,欢迎转载。旨在让人们关注996制度的存在和非法性,并尝试改变这种现象。

根据1994年第八届全国人大常委会通过和2018年第十三届全国人大常委会修正的《中华人民共和国劳动法》规定,劳动者每日工作时间不超过8小时,平均每周工作时间不超过44小时,而996工作制每周至少要工作72个小时,远超法律标准,因此996工作制度违反劳动法。

而钉钉却允许企业管理者违反法律,非法排班!

第三十六条 国家实行劳动者每日工作时间不超过八小时、平均每周工作时间不超过四十四小时的工时制度。

第四十一条 用人单位由于生产经营需要,经与工会和劳动者协商后可以延长工作时间,一般每日不得超过一小时;因特殊原因需要延长工作时间的,在保障劳动者身体健康的条件下延长工作时间每日不得超过三小时,但是每月不得超过三十六小时。

第四十四条 有下列情形之一的,用人单位应当按照下列标准支付高于劳动者正常工作时间工资的工资报酬:

(一)安排劳动者延长工作时间的,支付不低于工资的百分之一百五十的工资报酬;
(二)休息日安排劳动者工作又不能安排补休的,支付不低于工资的百分之二百的工资报酬;
(三)法定休假日安排劳动者工作的,支付不低于工资的百分之三百的工资报酬。

第九十条 用人单位违反本法规定,延长劳动者工作时间的,由劳动行政部门给予警告,责令改正,并可以处以罚款。

第九十一条 用人单位有下列侵害劳动者合法权益情形之一的,由劳动行政部门责令支付劳动者的工资报酬、经济补偿,并可以责令支付赔偿金:

(二)拒不支付劳动者延长工作时间工资报酬的;

相关项目:996 薪资计算助手


如果觉得还不错的话,就点击右上角, 给我个Star ⭐️ 鼓励一下我吧~

钉钉全自动打卡

声明

本篇文章为摘录自GitHub,自己修改了部分代码,仅作为参考,详见原文

⏰ 定时脚本

同时兼容定时和通知两种功能

  • 发送 “开启循环” 开启定时功能
  • 发送 “关闭循环” 关闭定时功能
  • 修正 “日志” 功能BUG,发送日志文件到邮箱中
  • 添加 “连接” 功能,实现远程连接电脑 AutoJs Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
/*
* @Author: George Huan
* @Date: 2020-08-03 09:30:30
* @LastEditTime: 2022-03-26 10:56:25
* @Description: DingDing-Automatic-Clock-in (Run on AutoJs)
* @URL: https://github.com/georgehuan1994/DingDing-Automatic-Clock-in
*/

const ACCOUNT = "15868141080"
const PASSWORD = ""

const QQ = "2757412961"
const EMAILL_ADDRESS = "2757412961@qq.com"
const SERVER_CHAN = "SCT171904TfSMLDvHz2oGuB8NU9NdMg1W0"
const PUSH_DEER = "PushDeer发送密钥"

const PUSH_METHOD = {QQ: 1, Email: 2, ServerChan: 3, PushDeer: 4}

// 默认通信方式:
// PUSH_METHOD.QQ -- QQ
// PUSH_METHOD.Email -- Email
// PUSH_METHOD.ServerChan -- Server酱
// PUSH_METHOD.PushDeer -- Push Deer
var DEFAULT_MESSAGE_DELIVER = PUSH_METHOD.QQ;

const PACKAGE_ID_QQ = "com.tencent.mobileqq" // QQ
// const PACKAGE_ID_DD = "com.alibaba.android.rimet" // 钉钉
const PACKAGE_ID_DD = "com.alibaba.android.rimet.zju" // 浙大钉
const PACKAGE_ID_XMSF = "com.xiaomi.xmsf" // 小米推送服务
const PACKAGE_ID_TASKER = "net.dinglisch.android.taskerm" // Tasker
const PACKAGE_ID_MAIL_163 = "com.netease.mail" // 网易邮箱大师
const PACKAGE_ID_MAIL_ANDROID = "com.android.email" // 系统内置邮箱
const PACKAGE_ID_PUSHDEER = "com.pushdeer.os" // Push Deer

const LOWER_BOUND = 1 * 60 * 1000 // 最小等待时间:1min
const UPPER_BOUND = 5 * 60 * 1000 // 最大等待时间:5min
const ONE_MIN = 1 * 60 * 1000 // 等待时间:1min
const FIF_MIN = 15 * 60 * 1000 // 等待时间:0.5h
const HALF_AN_HOUR = 30 * 60 * 1000 // 等待时间:0.5h
const AN_HOUR = 1 * 60 * 60 * 1000 // 等待时间:1h

// 执行时的屏幕亮度(0-255), 需要"修改系统设置"权限
const SCREEN_BRIGHTNESS = 20

// 是否过滤通知
const NOTIFICATIONS_FILTER = true

// PackageId白名单
const PACKAGE_ID_WHITE_LIST = [PACKAGE_ID_QQ,PACKAGE_ID_DD,PACKAGE_ID_XMSF,PACKAGE_ID_MAIL_163,PACKAGE_ID_TASKER,PACKAGE_ID_PUSHDEER]

// 公司的钉钉CorpId, 获取方法见 2020-09-24 更新日志。如果只加入了一家公司, 可以不填
// dingtalk://dingtalkclient/page/link?url='https://attend.dingtalk.com/attend/index.html?corpId=dingca07f56daee3def1bc961a6cb783455b'
const CORP_ID = "dingca07f56daee3def1bc961a6cb783455b"

// 锁屏意图, 配合 Tasker 完成锁屏动作, 具体配置方法见 2021-03-09 更新日志
const ACTION_LOCK_SCREEN = "autojs.intent.action.LOCK_SCREEN"

// 监听音量+键, 开启后无法通过音量+键调整音量, 按下音量+键:结束所有子线程
const OBSERVE_VOLUME_KEY = true

const WEEK_DAY = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday",]


// =================== ↓↓↓ 主线程:监听通知 ↓↓↓ ====================

var currentDate = new Date()

// 是否暂停循环打卡
var recursiveCard = false
var timeAnchorPoints = [8,11,13,17,19,22]

// 是否暂停定时打卡
var suspend = false

// 本次打开钉钉前是否需要等待
var needWaiting = false

// 运行日志路径
var globalLogFilePath = "/sdcard/脚本/Archive/" + getCurrentDate() + "-log.txt"

// 检查无障碍权限
auto.waitFor("normal")

// 检查Autojs版本
requiresAutojsVersion("4.1.0")

// 创建运行日志
console.setGlobalLogConfig({
file: "/sdcard/脚本/Archive/" + getCurrentDate() + "-log.txt"
});

// 监听本机通知
events.observeNotification()
events.on("notification", function(n) {
notificationHandler(n)
});

events.setKeyInterceptionEnabled("volume_up", OBSERVE_VOLUME_KEY)

if (OBSERVE_VOLUME_KEY) {
events.observeKey()
};

// 监听音量+键
events.onKeyDown("volume_up", function(event){
threads.shutDownAll()
device.setBrightnessMode(1)
device.cancelKeepingAwake()
toast("已中断所有子线程!")

// 可以在此调试各个方法
// doClock()
// sendQQMsg("测试文本")
// sendEmail("测试主题", "测试文本", null)
// sendServerChan(测试主题, 测试文本)
// sendPushDeer(测试主题, 测试文本)
});

toastLog("监听中, 请在日志中查看记录的通知及其内容")

// =================== ↑↑↑ 主线程:监听通知 ↑↑↑ =====================


// =================== ↑↑↑ 副线程:循环打卡 ↑↑↑ =====================
startRecursiveCard();

function startRecursiveCard(){
while(recursiveCard){
var hour = new Date().getHours()
// 直到打卡时间点 启动钉钉
while(recursiveCard && !existsInArray(timeAnchorPoints, hour)){
var randomTime = random(ONE_MIN, FIF_MIN)
console.log("【循环打卡】未到时间点,进行" + Math.floor(randomTime / 1000) + "秒随机睡眠...")
sleep(randomTime)
hour = new Date().getHours()
}

doClock(); // 打卡
console.log(new Date() + " 【循环打卡】打卡成功")
if(DEFAULT_MESSAGE_DELIVER == PUSH_METHOD.QQ){
sleep(10000) // 等待默认消息通知程序发送
sendQQMsg(new Date() + " 【循环打卡】打卡成功")
sendServerChan("【循环打卡】打卡结果", new Date() + " 打卡成功")
console.log("【循环打卡】QQ&ServerChan 消息发送成功...")
}

// 直到打卡时间点 启动钉钉
while(recursiveCard && existsInArray(timeAnchorPoints, hour)){
var randomTime = random(ONE_MIN, FIF_MIN)
console.log("【循环打卡】已打卡,进行" + Math.floor(randomTime / 1000) + "秒随机睡眠...")
sleep(randomTime)
hour = new Date().getHours()
}
};
}
// =================== ↑↑↑ 副线程:循环打卡 ↑↑↑ =====================

/**
* @description 处理通知
*/
function notificationHandler(n) {

var packageId = n.getPackageName() // 获取通知包名
var abstract = n.tickerText // 获取通知摘要
var text = n.getText() // 获取通知文本

// 过滤 PackageId 白名单之外的应用所发出的通知
if (!filterNotification(packageId, abstract, text)) {
return;
}

// 监听摘要为 "定时打卡" 的通知, 不一定要从 Tasker 中发出通知, 日历、定时器等App均可实现
if (abstract == "定时打卡" && !suspend) {
needWaiting = true
threads.shutDownAll()
threads.start(function(){
doClock()
})
return;
}

switch(text) {

case "打卡": // 监听文本为 "打卡" 的通知
needWaiting = false
// threads.shutDownAll()
threads.start(function(){
doClock()
})
break;

case "查询": // 监听文本为 "查询" 的通知
// threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg(getStorageData("dingding", "clockResult"))
break;
case PUSH_METHOD.Email:
sendEmail("考勤结果", getStorageData("dingding", "clockResult"), null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("考勤结果", getStorageData("dingding", "clockResult"))
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("考勤结果", getStorageData("dingding", "clockResult"))
break;
}
})
break;

case "暂停": // 监听文本为 "暂停" 的通知
suspend = true
console.warn("暂停定时打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("修改成功, 已暂停定时打卡功能")
break;
case PUSH_METHOD.Email:
sendEmail("修改成功", "已暂停定时打卡功能", null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("修改成功", "已暂停定时打卡功能")
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("修改成功", "已暂停定时打卡功能")
break;
}
})
break;

case "恢复": // 监听文本为 "恢复" 的通知
suspend = false
console.warn("恢复定时打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("修改成功, 已恢复定时打卡功能")
break;
case PUSH_METHOD.Email:
sendEmail("修改成功", "已恢复定时打卡功能", null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("修改成功", "已恢复定时打卡功能")
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("修改成功", "已恢复定时打卡功能")
break;
}
})
break;

case "日志": // 监听文本为 "日志" 的通知
// threads.shutDownAll()
threads.start(function(){
sendEmail("获取日志", globalLogFilePath, globalLogFilePath)
})
break;


case "连接": // 连接到 Autojs Server
// threads.shutDownAll()
threads.start(function(){
connectServer()
})
break;

case "开启循环": // 每天循环打卡 早八晚十
recursiveCard = true
console.warn("每天循环打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("开启每天循环打卡功能(早八晚十),时间点:" + timeAnchorPoints)
break;
case PUSH_METHOD.Email:
sendEmail("开启每天循环打卡功能(早八晚十),时间点:" + timeAnchorPoints, null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("开启循环打卡成功", "开启每天循环打卡功能(早八晚十),时间点:" + timeAnchorPoints)
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("开启循环打卡成功", "开启每天循环打卡功能(早八晚十),时间点:" + timeAnchorPoints)
break;
}
startRecursiveCard();
})
break;

case "关闭循环": // 每天循环打卡 早八晚十
recursiveCard = false
console.warn("关闭每天循环打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("关闭每天循环打卡功能")
break;
case PUSH_METHOD.Email:
sendEmail("关闭每天循环打卡功能", null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("关闭循环打卡成功", "关闭每天循环打卡功能")
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("关闭循环打卡成功", "关闭每天循环打卡功能")
break;
}
})
break;

default:
break;
}

if (text == null)
return;

// 监听钉钉返回的考勤结果
// if (packageId == PACKAGE_ID_DD && text.indexOf("考勤打卡") >= 0) {
if (packageId == PACKAGE_ID_DD) {
setStorageData("dingding", "clockResult", text)
console.warn("监听钉钉返回的消息...")
// threads.shutDownAll()
threads.start(function() {
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg(text)
break;
case PUSH_METHOD.Email:
sendEmail("考勤结果", text, cameraFilePath)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("考勤结果", text)
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("考勤结果", text)
break;
}
})
return;
}
}


/**
* @description 打卡流程
*/
function doClock() {

currentDate = new Date()
console.log("本地时间: " + getCurrentDate() + " " + getCurrentTime())
console.log("开始打卡流程!")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕
holdOn() // 随机等待
signIn() // 自动登录
handleLate() // 处理迟到
attendKaoqin() // 考勤打卡

if (null != textContains("上班打卡").findOne(1000))
clockIn() // 上班打卡
else if (null != textContains("下班打卡").findOne(1000))
clockOut() // 下班打卡

lockScreen() // 关闭屏幕
}


/**
* @description 发送邮件流程
* @param {string} title 邮件主题
* @param {string} message 邮件正文
* @param {string} attachFilePath 要发送的附件路径
*/
function sendEmail(title, message, attachFilePath) {

console.log("开始发送邮件流程!")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕


// 内置电子邮件
app.launch("com.android.email");
console.log("等待邮箱启动...")
sleep(3000) // 等待邮箱启动

if(attachFilePath != null && files.exists(attachFilePath)) {
console.info(attachFilePath)
app.sendEmail({
email: [EMAILL_ADDRESS], subject: title, text: message, attachment: "file://" + attachFilePath
})
}
else {
console.error(attachFilePath)
app.sendEmail({
email: [EMAILL_ADDRESS], subject: title, text: message
})
}
sleep(1000)

// 选择邮箱应用,系统默认应用即可
var emailText = text("电子邮件").findOne(1000);
if(null == emailText){
console.log("邮箱应用选择失败...")
return;
}
emailText.parent().click();
console.log("邮箱应用选择成功...")
sleep(1000)

// 点击发送按钮
var sendBtn = desc("发送").findOne(1000);
if(null == sendBtn){
console.log("邮箱发送失败...")
return;
}
sendBtn.click()
console.log("正在发送邮件...")

// =============================================================================================================
// console.log("选择邮件应用")
// waitForActivity("com.android.internal.app.ChooserActivity") // 等待选择应用界面弹窗出现, 如果设置了默认应用就注释掉
// id("compose_send_btn").findOne().click()

// var emailAppName = app.getAppName(PACKAGE_ID_MAIL_163)
// if (null != emailAppName) {
// if (null != textMatches(emailAppName).findOne(1000)) {
// btn_email = textMatches(emailAppName).findOnce().parent()
// btn_email.click()
// }
// }
// else {
// console.error("不存在应用: " + PACKAGE_ID_MAIL_163)
// lockScreen()
// return;
// }

// // 网易邮箱大师
// var versoin = getPackageVersion(PACKAGE_ID_MAIL_163)
// console.log("应用版本: " + versoin)
// var sp = versoin.split(".")
// if (sp[0] == 6) {
// // 网易邮箱大师 6
// waitForActivity("com.netease.mobimail.activity.MailComposeActivity")
// id("send").findOne().click()
// }
// else {
// // 网易邮箱大师 7
// waitForActivity("com.netease.mobimail.module.mailcompose.MailComposeActivity")
// var input_address = id("input").findOne()
// if (null == input_address.getText()) {
// input_address.setText(EMAILL_ADDRESS)
// }
// id("iv_arrow").findOne().click()
// sleep(1000)
// id("img_send_bg").findOne().click()
// }


home()
sleep(2000)
lockScreen() // 关闭屏幕
}


/**
* @description 发送QQ消息
* @param {string} message 消息内容
*/
function sendQQMsg(message) {

console.log("发送QQ消息")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕

app.startActivity({
action: "android.intent.action.VIEW",
data:"mqq://im/chat?chat_type=wpa&version=1&src_type=web&uin=" + QQ,
packageName: "com.tencent.mobileqq",
});

// waitForActivity("com.tencent.mobileqq.activity.SplashActivity")

id("input").findOne().setText(message)
id("fun_btn").findOne().click()

home()
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description ServerChan推送
* @param {string} title 标题
* @param {string} message 消息
*/
function sendServerChan(title, message) {

console.log("向 ServerChan 发起推送请求")

url = "https://sctapi.ftqq.com/" + SERVER_CHAN + ".send";

res = http.post(encodeURI(url), {
"title": title,
"desp": message
});

console.log(res)
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description PushDeer推送
* @param {string} title 标题
* @param {string} message 消息
*/
function sendPushDeer(title, message) {

console.log("向 PushDeer 发起推送请求")

url = "https://api2.pushdeer.com/message/push"

res = http.post(encodeURI(url), {
"pushkey": PUSH_DEER,
"text": title,
"desp": message,
"type": "markdown",
});

console.log(res)
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description 唤醒设备
*/
function brightScreen() {

console.log("唤醒设备")

device.setBrightnessMode(0) // 手动亮度模式
device.setBrightness(SCREEN_BRIGHTNESS)
device.wakeUpIfNeeded() // 唤醒设备
device.keepScreenOn() // 保持亮屏
sleep(1000) // 等待屏幕亮起

if (!device.isScreenOn()) {
console.warn("设备未唤醒, 重试")
device.wakeUpIfNeeded()
brightScreen()
}
else {
console.info("设备已唤醒")
}
sleep(1000)
}


/**
* @description 解锁屏幕
*/
function unlockScreen() {

console.log("解锁屏幕")

if (isDeviceLocked()) {

gesture(
320, // 滑动时间:毫秒
[
device.width * 0.5, // 滑动起点 x 坐标:屏幕宽度的一半
device.height * 0.9 // 滑动起点 y 坐标:距离屏幕底部 10% 的位置, 华为系统需要往上一些
],
[
device.width / 2, // 滑动终点 x 坐标:屏幕宽度的一半
device.height * 0.1 // 滑动终点 y 坐标:距离屏幕顶部 10% 的位置
]
)

sleep(1000) // 等待解锁动画完成
home()
sleep(1000) // 等待返回动画完成
}

if (isDeviceLocked()) {
console.error("上滑解锁失败, 请按脚本中的注释调整 gesture(time, [x1,y1], [x2,y2]) 方法的参数!")
return;
}
console.info("屏幕已解锁")
}


/**
* @description 随机等待
*/
function holdOn(){

if (!needWaiting) {
return;
}

var randomTime = random(LOWER_BOUND, UPPER_BOUND)
toastLog(Math.floor(randomTime / 1000) + "秒后启动" + app.getAppName(PACKAGE_ID_DD) + "...")
sleep(randomTime)
}


/**
* @description 启动并登陆钉钉
*/
function signIn() {

app.launchPackage(PACKAGE_ID_DD)
console.log("正在启动" + app.getAppName(PACKAGE_ID_DD) + "...")

setVolume(0) // 设备静音

sleep(10000) // 等待钉钉启动

if (currentPackage() == PACKAGE_ID_DD &&
currentActivity() == "com.alibaba.android.user.login.SignUpWithPwdActivity") {
console.info("账号未登录")

var account = id("et_phone_input").findOne()
account.setText(ACCOUNT)
console.log("输入账号")

var password = id("et_pwd_login").findOne()
password.setText(PASSWORD)
console.log("输入密码")

var privacy = id("cb_privacy").findOne(1000)
if(null != privacy){
privacy.click()
console.log("同意隐私协议")
}

var btn_login = id("btn_next").findOne()
btn_login.click()
console.log("正在登陆...")

sleep(3000)
}

if (currentPackage() == PACKAGE_ID_DD &&
currentActivity() != "com.alibaba.android.user.login.SignUpWithPwdActivity") {
console.info("账号已登录")
sleep(1000)
}
}


/**
* @description 处理迟到打卡
*/
function handleLate(){

if (null != textMatches("迟到打卡").clickable(true).findOne(1000)) {
btn_late = textMatches("迟到打卡").clickable(true).findOnce()
btn_late.click()
console.warn("迟到打卡")
}
if (null != descMatches("迟到打卡").clickable(true).findOne(1000)) {
btn_late = descMatches("迟到打卡").clickable(true).findOnce()
btn_late.click()
console.warn("迟到打卡")
}
}


/**
* @description 使用 URL Scheme 进入考勤界面
*/
function attendKaoqin(){

var url_scheme = "dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html"

if(CORP_ID != "") {
url_scheme = url_scheme + "?corpId=" + CORP_ID
}

var a = app.intent({
// action: "VIEW",
action: "android.intent.action.VIEW",
data: url_scheme,
//flags: [Intent.FLAG_ACTIVITY_NEW_TASK]
});
app.startActivity(a);
console.log("正在进入考勤界面...")

textContains("申请").waitFor()
console.info("已进入考勤界面")
sleep(8000)
}


/**
* @description 上班打卡
*/
function clockIn() {

console.log("上班打卡...")

// if (null != textContains("已打卡").findOne(1000)) {
// console.info("已打卡")
// toast("已打卡")
// home()
// sleep(1000)
// return;
// }

console.log("等待连接到考勤机...")
sleep(2000)

if (null != textContains("未连接").findOne(1000)) {
console.error("未连接考勤机, 重新进入考勤界面!")
back()
sleep(2000)
attendKaoqin()
return;
}

textContains("已连接").waitFor()
console.info("已连接考勤机")
sleep(1000)

if (null != textMatches("上班打卡").clickable(true).findOne(1000)) {
btn_clockin = textMatches("上班打卡").clickable(true).findOnce()
btn_clockin.click()
console.log("按下打卡按钮")
}
else {
click(device.width / 2, device.height * 0.560)
console.log("点击打卡按钮坐标")
}
sleep(1000)
handleLate() // 处理迟到打卡

home()
sleep(1000)
}


/**
* @description 下班打卡
*/
function clockOut() {

console.log("下班打卡...")
console.log("等待连接到考勤机...")
sleep(2000)

if (null != textContains("未连接").findOne(1000)) {
console.error("未连接考勤机, 重新进入考勤界面!")
back()
sleep(2000)
attendKaoqin()
return;
}

textContains("已连接").waitFor()
console.info("已连接考勤机")
sleep(1000)

if (null != textMatches("下班打卡").clickable(true).findOne(1000)) {
btn_clockout = textMatches("下班打卡").clickable(true).findOnce()
btn_clockout.click()
console.log("按下打卡按钮")
sleep(1000)
}
else {
click(device.width / 2, device.height * 0.560)
console.log("点击打卡按钮坐标")
}

if (null != textContains("早退打卡").clickable(true).findOne(1000)) {
className("android.widget.Button").text("早退打卡").clickable(true).findOnce().parent().click()
console.warn("早退打卡")
}

home()
sleep(1000)
}


/**
* @description 锁屏
*/
function lockScreen(){

console.log("关闭屏幕")

// 锁屏方案1:Root
// Power()

// 锁屏方案2:No Root
// press(Math.floor(device.width / 2), Math.floor(device.height * 0.973), 1000) // 小米的快捷手势:长按Home键锁屏

// 万能锁屏方案:向Tasker发送广播, 触发系统锁屏动作。配置方法见 2021-03-09 更新日志
app.sendBroadcast({action: ACTION_LOCK_SCREEN});

device.setBrightnessMode(1) // 自动亮度模式
device.cancelKeepingAwake() // 取消设备常亮

if (isDeviceLocked()) {
console.info("屏幕已关闭")
}
else {
console.error("屏幕未关闭, 请尝试其他锁屏方案, 或等待屏幕自动关闭")
}
}


/**
* @description 连接 AutoJs Server
*/
function connectServer(){

console.log("开始连接 AutoJs Server!")
console.log("本地时间: " + getCurrentDate() + " " + getCurrentTime())

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕

connectAutoJsServer() // 连接 AutoJs Server

lockScreen() // 关闭屏幕

}

/**
*
* @description 启动 AutoJs Server 整体流程
*/
function connectAutoJsServer(){

launchApp("Auto.js")
console.log("正在启动 Auto.js...")
sleep(10000) // 等待 Auto.js 启动

// 打开侧拉菜单
var menuBtn = className("android.widget.ImageButton").desc("打开侧拉菜单").clickable(true).findOne(1000);
if(null != menuBtn){
menuBtn.click();
}
console.log("打开侧拉菜单...")
sleep(1000)

// 滚动屏幕找到连接电脑按钮
var menu = id("drawer_menu").findOne(1000);
if(null == menu){
console.log("滚动屏幕未找到连接电脑按钮...")
return;
}

menu.scrollForward()
console.log("滚动屏幕找到连接电脑按钮...")
sleep(1000)

// 连接电脑
var textCon = className("android.widget.TextView").text("连接电脑").findOne(1000);
if(null != textCon){
// 寻找按钮(Switch)
var layerCon = textCon.parent();
var switchBtn = layerCon.children().findOne(className("android.widget.Switch"));
console.log("找到连接电脑按钮,按钮状态为:" + switchBtn.text() + " " + switchBtn.checked() + "...")
if(switchBtn.checked() == false){
// 按下按钮
switchBtn.click();
console.log("开始连接电脑...")
sleep(2000)

// 点击确定
var okBtn = text("确定").findOne(1000);
okBtn.click();
console.log("连接电脑成功...")
}
}

sleep(1000) // 等待解锁动画完成
home()
sleep(1000) // 等待返回动画完成

// console.log("==========================================================");
// id("drawer_menu").findOne().children().forEach(child => {
// var target = child.findOne(id("sw"));
// console.log(target);
// });
}


// ===================== ↓↓↓ 功能函数 ↓↓↓ =======================

function dateDigitToString(num){
return num < 10 ? '0' + num : num
}

function getCurrentTime(){
var currentDate = new Date()
var hours = dateDigitToString(currentDate.getHours())
var minute = dateDigitToString(currentDate.getMinutes())
var second = dateDigitToString(currentDate.getSeconds())
var formattedTimeString = hours + ':' + minute + ':' + second
return formattedTimeString
}

function getCurrentDate(){
var currentDate = new Date()
var year = dateDigitToString(currentDate.getFullYear())
var month = dateDigitToString(currentDate.getMonth() + 1)
var date = dateDigitToString(currentDate.getDate())
var week = currentDate.getDay()
var formattedDateString = year + '-' + month + '-' + date + '-' + WEEK_DAY[week]
return formattedDateString
}

function existsInArray(arr, element){
for(var i=0; i<arr.length; i++){
if(arr[i] == element) return true;
}
return false;
}

// 通知过滤器
function filterNotification(bundleId, abstract, text) {
var check = PACKAGE_ID_WHITE_LIST.some(function(item) {return bundleId == item})
if (!NOTIFICATIONS_FILTER || check) {
console.verbose(bundleId)
console.verbose(abstract)
console.verbose(text)
console.verbose("---------------------------")
return true
}
else {
return false
}
}

// 保存本地数据
function setStorageData(name, key, value) {
const storage = storages.create(name) // 创建storage对象
storage.put(key, value)
}

// 读取本地数据
function getStorageData(name, key) {
const storage = storages.create(name)
if (storage.contains(key)) {
return storage.get(key, "")
}
// 默认返回undefined
}

// 删除本地数据
function delStorageData(name, key) {
const storage = storages.create(name)
if (storage.contains(key)) {
storage.remove(key)
}
}

// 获取应用版本号
function getPackageVersion(bundleId) {
importPackage(android.content)
var pckMan = context.getPackageManager()
var packageInfo = pckMan.getPackageInfo(bundleId, 0)
return packageInfo.versionName
}

// 屏幕是否为锁定状态
function isDeviceLocked() {
importClass(android.app.KeyguardManager)
importClass(android.content.Context)
var km = context.getSystemService(Context.KEYGUARD_SERVICE)
return km.isKeyguardLocked()
}

// 设置媒体和通知音量
function setVolume(volume) {
device.setMusicVolume(volume)
device.setNotificationVolume(volume)
console.verbose("媒体音量:" + device.getMusicVolume())
console.verbose("通知音量:" + device.getNotificationVolume())
}

Auto.js-VSCodeExt README

桌面编辑器Visual Studio Code的插件。可以让Visual Studio Code支持Auto.js开发。

Install

在VS Code中菜单”查看”->”扩展”->输入”Auto.js”或”hyb1996”搜索,即可看到”Auto.js-VSCodeExt”插件,安装即可。插件的更新也可以在这里更新。

Features

目前功能比较基础,仅支持:

  • 在VS Code的开发者工具实时显示Auto.js的日志与输出
  • 在VS Code命令中增加Run, Stop, Rerun, Stop all等选项。可以在手机与电脑连接后把Sublime编辑器中的脚本推送到AutoJs中执行,或者停止AutoJs中运行的脚本。

Usage

Step 1

Ctrl+Shift+P 或点击”查看”->”命令面板”可调出命令面板,输入 Auto.js 可以看到几个命令,移动光标到命令Auto.js: Start Server,按回车键执行该命令。

此时VS Code会在右上角显示”Auto.js server running”,即开启服务成功。

Step 2

将手机连接到电脑启用的Wifi或者同一局域网中。通过命令行ipconfig(或者其他操作系统的相同功能命令)查看电脑的IP地址。在Auto.js的侧拉菜单中启用调试服务,并输入IP地址,等待连接成功。

Step 3

之后就可以在电脑上编辑JavaScript文件并通过命令Run或者按键F5在手机上运行了。

Commands

Ctrl+Shift+P 或点击”查看”->”命令面板”可调出命令面板,输入 Auto.js 可以看到几个命令:

  • Start Server: 启动插件服务。之后在确保手机和电脑在同一区域网的情况下,在Auto.js的侧拉菜单中使用连接电脑功能连接。
  • Stop Server: 停止插件服务。
  • Run 运行当前编辑器的脚本。如果有多个设备连接,则在所有设备运行。
  • Rerun 停止当前文件对应的脚本并重新运行。如果有多个设备连接,则在所有设备重新运行。
  • Stop 停止当前文件对应的脚本。如果有多个设备连接,则在所有设备停止。
  • StopAll 停止所有正在运行的脚本。如果有多个设备连接,则在所有设备运行所有脚本。
  • Save 保存当前文件到手机的脚本默认目录(文件名会加上前缀remote)。如果有多个设备连接,则在所有设备保存。
  • RunOnDevice: 弹出设备菜单并在指定设备运行脚本。
  • SaveToDevice: 弹出设备菜单并在指定设备保存脚本。
  • New Project(新建项目):选择一个空文件夹(或者在文件管理器中新建一个空文件夹),将会自动创建一个项目
  • Run Project(运行项目):运行一个项目,需要Auto.js 4.0.4Alpha5以上支持
  • Save Project(保存项目):保存一个项目,需要Auto.js 4.0.4Alpha5以上支持

以上命令一些有对应的快捷键,参照命令后面的说明即可。

Log

要显示来自Auto.js的日志,打开 VS Code上面菜单的”帮助”->”切换开发人员工具”->”Console”即可。

钉钉打卡脚本原文

⏰ DingDing-Automatic-Clock-in

📖 简介

钉钉全自动打卡 + 远程打卡脚本,无需 root,基于 auto.js,适用于蓝牙考勤机。

💥 功能

  • 定时打卡
  • 远程打卡
  • 发送考勤结果

⚙️ 工具

  • auto.js
  • Tasker
  • 一款通讯应用(示例脚本中使用的是 QQ / 网易邮箱大师 / ServerChan / PushDeer,彼此互为备用方案)

💡 原理

通过 auto.js 脚本监听本机通知,在 Tasker 中创建定时任务,发出通知,或在另一设备上发送消息到本机,即可触发脚本中的打卡进程,实现定时打卡和远程打卡。

image

同理,监听到钉钉发出的打卡成功通知后,将通知文本通过 QQ消息 或 邮件正文 发送,实现发送考勤结果的功能。

📝 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
/*
* @Author: George Huan
* @Date: 2020-08-03 09:30:30
* @LastEditTime: 2022-03-26 10:56:25
* @Description: DingDing-Automatic-Clock-in (Run on AutoJs)
* @URL: https://github.com/georgehuan1994/DingDing-Automatic-Clock-in
*/

const ACCOUNT = "钉钉账号"
const PASSWORD = "钉钉密码"

const QQ = "用于接收打卡结果的QQ号"
const EMAILL_ADDRESS = "用于接收打卡结果的邮箱地址"
const SERVER_CHAN = "Server酱发送密钥"
const PUSH_DEER = "PushDeer发送密钥"

const PUSH_METHOD = {QQ: 1, Email: 2, ServerChan: 3, PushDeer: 4}

// 默认通信方式:
// PUSH_METHOD.QQ -- QQ
// PUSH_METHOD.Email -- Email
// PUSH_METHOD.ServerChan -- Server酱
// PUSH_METHOD.PushDeer -- Push Deer
var DEFAULT_MESSAGE_DELIVER = PUSH_METHOD.QQ;

const PACKAGE_ID_QQ = "com.tencent.mobileqq" // QQ
const PACKAGE_ID_DD = "com.alibaba.android.rimet" // 钉钉
const PACKAGE_ID_XMSF = "com.xiaomi.xmsf" // 小米推送服务
const PACKAGE_ID_TASKER = "net.dinglisch.android.taskerm" // Tasker
const PACKAGE_ID_MAIL_163 = "com.netease.mail" // 网易邮箱大师
const PACKAGE_ID_MAIL_ANDROID = "com.android.email" // 系统内置邮箱
const PACKAGE_ID_PUSHDEER = "com.pushdeer.os" // Push Deer

const LOWER_BOUND = 1 * 60 * 1000 // 最小等待时间:1min
const UPPER_BOUND = 5 * 60 * 1000 // 最大等待时间:5min

// 执行时的屏幕亮度(0-255), 需要"修改系统设置"权限
const SCREEN_BRIGHTNESS = 20

// 是否过滤通知
const NOTIFICATIONS_FILTER = true

// PackageId白名单
const PACKAGE_ID_WHITE_LIST = [PACKAGE_ID_QQ,PACKAGE_ID_DD,PACKAGE_ID_XMSF,PACKAGE_ID_MAIL_163,PACKAGE_ID_TASKER,PACKAGE_ID_PUSHDEER]

// 公司的钉钉CorpId, 获取方法见 2020-09-24 更新日志。如果只加入了一家公司, 可以不填
const CORP_ID = ""

// 锁屏意图, 配合 Tasker 完成锁屏动作, 具体配置方法见 2021-03-09 更新日志
const ACTION_LOCK_SCREEN = "autojs.intent.action.LOCK_SCREEN"

// 监听音量+键, 开启后无法通过音量+键调整音量, 按下音量+键:结束所有子线程
const OBSERVE_VOLUME_KEY = true

const WEEK_DAY = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday",]


// =================== ↓↓↓ 主线程:监听通知 ↓↓↓ ====================

var currentDate = new Date()

// 是否暂停定时打卡
var suspend = false

// 本次打开钉钉前是否需要等待
var needWaiting = true

// 运行日志路径
var globalLogFilePath = "/sdcard/脚本/Archive/" + getCurrentDate() + "-log.txt"

// 检查无障碍权限
auto.waitFor("normal")

// 检查Autojs版本
requiresAutojsVersion("4.1.0")

// 创建运行日志
console.setGlobalLogConfig({
file: "/sdcard/脚本/Archive/" + getCurrentDate() + "-log.txt"
});

// 监听本机通知
events.observeNotification()
events.on("notification", function(n) {
notificationHandler(n)
});

events.setKeyInterceptionEnabled("volume_up", OBSERVE_VOLUME_KEY)

if (OBSERVE_VOLUME_KEY) {
events.observeKey()
};

// 监听音量+键
events.onKeyDown("volume_up", function(event){
threads.shutDownAll()
device.setBrightnessMode(1)
device.cancelKeepingAwake()
toast("已中断所有子线程!")

// 可以在此调试各个方法
// doClock()
// sendQQMsg("测试文本")
// sendEmail("测试主题", "测试文本", null)
// sendServerChan(测试主题, 测试文本)
// sendPushDeer(测试主题, 测试文本)
});

toastLog("监听中, 请在日志中查看记录的通知及其内容")

// =================== ↑↑↑ 主线程:监听通知 ↑↑↑ =====================



/**
* @description 处理通知
*/
function notificationHandler(n) {

var packageId = n.getPackageName() // 获取通知包名
var abstract = n.tickerText // 获取通知摘要
var text = n.getText() // 获取通知文本

// 过滤 PackageId 白名单之外的应用所发出的通知
if (!filterNotification(packageId, abstract, text)) {
return;
}

// 监听摘要为 "定时打卡" 的通知, 不一定要从 Tasker 中发出通知, 日历、定时器等App均可实现
if (abstract == "定时打卡" && !suspend) {
needWaiting = true
threads.shutDownAll()
threads.start(function(){
doClock()
})
return;
}

switch(text) {

case "打卡": // 监听文本为 "打卡" 的通知
needWaiting = false
threads.shutDownAll()
threads.start(function(){
doClock()
})
break;

case "查询": // 监听文本为 "查询" 的通知
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg(getStorageData("dingding", "clockResult"))
break;
case PUSH_METHOD.Email:
sendEmail("考勤结果", getStorageData("dingding", "clockResult"), null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("考勤结果", getStorageData("dingding", "clockResult"))
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("考勤结果", getStorageData("dingding", "clockResult"))
break;
}
})
break;

case "暂停": // 监听文本为 "暂停" 的通知
suspend = true
console.warn("暂停定时打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("修改成功, 已暂停定时打卡功能")
break;
case PUSH_METHOD.Email:
sendEmail("修改成功", "已暂停定时打卡功能", null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("修改成功", "已暂停定时打卡功能")
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("修改成功", "已暂停定时打卡功能")
break;
}
})
break;

case "恢复": // 监听文本为 "恢复" 的通知
suspend = false
console.warn("恢复定时打卡")
threads.shutDownAll()
threads.start(function(){
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg("修改成功, 已恢复定时打卡功能")
break;
case PUSH_METHOD.Email:
sendEmail("修改成功", "已恢复定时打卡功能", null)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("修改成功", "已恢复定时打卡功能")
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("修改成功", "已恢复定时打卡功能")
break;
}
})
break;

case "日志": // 监听文本为 "日志" 的通知
threads.shutDownAll()
threads.start(function(){
sendEmail("获取日志", globalLogFilePath, globalLogFilePath)
})
break;

default:
break;
}

if (text == null)
return;

// 监听钉钉返回的考勤结果
if (packageId == PACKAGE_ID_DD && text.indexOf("考勤打卡") >= 0) {
setStorageData("dingding", "clockResult", text)
threads.shutDownAll()
threads.start(function() {
switch(DEFAULT_MESSAGE_DELIVER) {
case PUSH_METHOD.QQ:
sendQQMsg(text)
break;
case PUSH_METHOD.Email:
sendEmail("考勤结果", text, cameraFilePath)
break;
case PUSH_METHOD.ServerChan:
sendServerChan("考勤结果", text)
break;
case PUSH_METHOD.PushDeer:
sendPushDeer("考勤结果", text)
break;
}
})
return;
}
}


/**
* @description 打卡流程
*/
function doClock() {

currentDate = new Date()
console.log("本地时间: " + getCurrentDate() + " " + getCurrentTime())
console.log("开始打卡流程!")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕
holdOn() // 随机等待
signIn() // 自动登录
handleLate() // 处理迟到
attendKaoqin() // 考勤打卡

if (currentDate.getHours() <= 12)
clockIn() // 上班打卡
else
clockOut() // 下班打卡

lockScreen() // 关闭屏幕
}


/**
* @description 发送邮件流程
* @param {string} title 邮件主题
* @param {string} message 邮件正文
* @param {string} attachFilePath 要发送的附件路径
*/
function sendEmail(title, message, attachFilePath) {

console.log("开始发送邮件流程!")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕

if(attachFilePath != null && files.exists(attachFilePath)) {
console.info(attachFilePath)
app.sendEmail({
email: [EMAILL_ADDRESS], subject: title, text: message, attachment: attachFilePath
})
}
else {
console.error(attachFilePath)
app.sendEmail({
email: [EMAILL_ADDRESS], subject: title, text: message
})
}

console.log("选择邮件应用")
waitForActivity("com.android.internal.app.ChooserActivity") // 等待选择应用界面弹窗出现, 如果设置了默认应用就注释掉

var emailAppName = app.getAppName(PACKAGE_ID_MAIL_163)
if (null != emailAppName) {
if (null != textMatches(emailAppName).findOne(1000)) {
btn_email = textMatches(emailAppName).findOnce().parent()
btn_email.click()
}
}
else {
console.error("不存在应用: " + PACKAGE_ID_MAIL_163)
lockScreen()
return;
}

// 网易邮箱大师
var versoin = getPackageVersion(PACKAGE_ID_MAIL_163)
console.log("应用版本: " + versoin)
var sp = versoin.split(".")
if (sp[0] == 6) {
// 网易邮箱大师 6
waitForActivity("com.netease.mobimail.activity.MailComposeActivity")
id("send").findOne().click()
}
else {
// 网易邮箱大师 7
waitForActivity("com.netease.mobimail.module.mailcompose.MailComposeActivity")
var input_address = id("input").findOne()
if (null == input_address.getText()) {
input_address.setText(EMAILL_ADDRESS)
}
id("iv_arrow").findOne().click()
sleep(1000)
id("img_send_bg").findOne().click()
}

// 内置电子邮件
// waitForActivity("com.kingsoft.mail.compose.ComposeActivity")
// id("compose_send_btn").findOne().click()

console.log("正在发送邮件...")

home()
sleep(2000)
lockScreen() // 关闭屏幕
}


/**
* @description 发送QQ消息
* @param {string} message 消息内容
*/
function sendQQMsg(message) {

console.log("发送QQ消息")

brightScreen() // 唤醒屏幕
unlockScreen() // 解锁屏幕

app.startActivity({
action: "android.intent.action.VIEW",
data:"mqq://im/chat?chat_type=wpa&version=1&src_type=web&uin=" + QQ,
packageName: "com.tencent.mobileqq",
});

// waitForActivity("com.tencent.mobileqq.activity.SplashActivity")

id("input").findOne().setText(message)
id("fun_btn").findOne().click()

home()
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description ServerChan推送
* @param {string} title 标题
* @param {string} message 消息
*/
function sendServerChan(title, message) {

console.log("向 ServerChan 发起推送请求")

url = "https://sctapi.ftqq.com/" + SERVER_CHAN + ".send";

res = http.post(encodeURI(url), {
"title": title,
"desp": message
});

console.log(res)
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description PushDeer推送
* @param {string} title 标题
* @param {string} message 消息
*/
function sendPushDeer(title, message) {

console.log("向 PushDeer 发起推送请求")

url = "https://api2.pushdeer.com/message/push"

res = http.post(encodeURI(url), {
"pushkey": PUSH_DEER,
"text": title,
"desp": message,
"type": "markdown",
});

console.log(res)
sleep(1000)
lockScreen() // 关闭屏幕
}


/**
* @description 唤醒设备
*/
function brightScreen() {

console.log("唤醒设备")

device.setBrightnessMode(0) // 手动亮度模式
device.setBrightness(SCREEN_BRIGHTNESS)
device.wakeUpIfNeeded() // 唤醒设备
device.keepScreenOn() // 保持亮屏
sleep(1000) // 等待屏幕亮起

if (!device.isScreenOn()) {
console.warn("设备未唤醒, 重试")
device.wakeUpIfNeeded()
brightScreen()
}
else {
console.info("设备已唤醒")
}
sleep(1000)
}


/**
* @description 解锁屏幕
*/
function unlockScreen() {

console.log("解锁屏幕")

if (isDeviceLocked()) {

gesture(
320, // 滑动时间:毫秒
[
device.width * 0.5, // 滑动起点 x 坐标:屏幕宽度的一半
device.height * 0.9 // 滑动起点 y 坐标:距离屏幕底部 10% 的位置, 华为系统需要往上一些
],
[
device.width / 2, // 滑动终点 x 坐标:屏幕宽度的一半
device.height * 0.1 // 滑动终点 y 坐标:距离屏幕顶部 10% 的位置
]
)

sleep(1000) // 等待解锁动画完成
home()
sleep(1000) // 等待返回动画完成
}

if (isDeviceLocked()) {
console.error("上滑解锁失败, 请按脚本中的注释调整 gesture(time, [x1,y1], [x2,y2]) 方法的参数!")
return;
}
console.info("屏幕已解锁")
}


/**
* @description 随机等待
*/
function holdOn(){

if (!needWaiting) {
return;
}

var randomTime = random(LOWER_BOUND, UPPER_BOUND)
toastLog(Math.floor(randomTime / 1000) + "秒后启动" + app.getAppName(PACKAGE_ID_DD) + "...")
sleep(randomTime)
}


/**
* @description 启动并登陆钉钉
*/
function signIn() {

app.launchPackage(PACKAGE_ID_DD)
console.log("正在启动" + app.getAppName(PACKAGE_ID_DD) + "...")

setVolume(0) // 设备静音

sleep(10000) // 等待钉钉启动

if (currentPackage() == PACKAGE_ID_DD &&
currentActivity() == "com.alibaba.android.user.login.SignUpWithPwdActivity") {
console.info("账号未登录")

var account = id("et_phone_input").findOne()
account.setText(ACCOUNT)
console.log("输入账号")

var password = id("et_pwd_login").findOne()
password.setText(PASSWORD)
console.log("输入密码")

var privacy = id("cb_privacy").findOne()
privacy.click()
console.log("同意隐私协议")

var btn_login = id("btn_next").findOne()
btn_login.click()
console.log("正在登陆...")

sleep(3000)
}

if (currentPackage() == PACKAGE_ID_DD &&
currentActivity() != "com.alibaba.android.user.login.SignUpWithPwdActivity") {
console.info("账号已登录")
sleep(1000)
}
}


/**
* @description 处理迟到打卡
*/
function handleLate(){

if (null != textMatches("迟到打卡").clickable(true).findOne(1000)) {
btn_late = textMatches("迟到打卡").clickable(true).findOnce()
btn_late.click()
console.warn("迟到打卡")
}
if (null != descMatches("迟到打卡").clickable(true).findOne(1000)) {
btn_late = descMatches("迟到打卡").clickable(true).findOnce()
btn_late.click()
console.warn("迟到打卡")
}
}


/**
* @description 使用 URL Scheme 进入考勤界面
*/
function attendKaoqin(){

var url_scheme = "dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html"

if(CORP_ID != "") {
url_scheme = url_scheme + "?corpId=" + CORP_ID
}

var a = app.intent({
action: "VIEW",
data: url_scheme,
//flags: [Intent.FLAG_ACTIVITY_NEW_TASK]
});
app.startActivity(a);
console.log("正在进入考勤界面...")

textContains("申请").waitFor()
console.info("已进入考勤界面")
sleep(1000)
}


/**
* @description 上班打卡
*/
function clockIn() {

console.log("上班打卡...")

if (null != textContains("已打卡").findOne(1000)) {
console.info("已打卡")
toast("已打卡")
home()
sleep(1000)
return;
}

console.log("等待连接到考勤机...")
sleep(2000)

if (null != textContains("未连接").findOne(1000)) {
console.error("未连接考勤机, 重新进入考勤界面!")
back()
sleep(2000)
attendKaoqin()
return;
}

textContains("已连接").waitFor()
console.info("已连接考勤机")
sleep(1000)

if (null != textMatches("上班打卡").clickable(true).findOne(1000)) {
btn_clockin = textMatches("上班打卡").clickable(true).findOnce()
btn_clockin.click()
console.log("按下打卡按钮")
}
else {
click(device.width / 2, device.height * 0.560)
console.log("点击打卡按钮坐标")
}
sleep(1000)
handleLate() // 处理迟到打卡

home()
sleep(1000)
}


/**
* @description 下班打卡
*/
function clockOut() {

console.log("下班打卡...")
console.log("等待连接到考勤机...")
sleep(2000)

if (null != textContains("未连接").findOne(1000)) {
console.error("未连接考勤机, 重新进入考勤界面!")
back()
sleep(2000)
attendKaoqin()
return;
}

textContains("已连接").waitFor()
console.info("已连接考勤机")
sleep(1000)

if (null != textMatches("下班打卡").clickable(true).findOne(1000)) {
btn_clockout = textMatches("下班打卡").clickable(true).findOnce()
btn_clockout.click()
console.log("按下打卡按钮")
sleep(1000)
}
else {
click(device.width / 2, device.height * 0.560)
console.log("点击打卡按钮坐标")
}

if (null != textContains("早退打卡").clickable(true).findOne(1000)) {
className("android.widget.Button").text("早退打卡").clickable(true).findOnce().parent().click()
console.warn("早退打卡")
}

home()
sleep(1000)
}


/**
* @description 锁屏
*/
function lockScreen(){

console.log("关闭屏幕")

// 锁屏方案1:Root
// Power()

// 锁屏方案2:No Root
// press(Math.floor(device.width / 2), Math.floor(device.height * 0.973), 1000) // 小米的快捷手势:长按Home键锁屏

// 万能锁屏方案:向Tasker发送广播, 触发系统锁屏动作。配置方法见 2021-03-09 更新日志
app.sendBroadcast({action: ACTION_LOCK_SCREEN});

device.setBrightnessMode(1) // 自动亮度模式
device.cancelKeepingAwake() // 取消设备常亮

if (isDeviceLocked()) {
console.info("屏幕已关闭")
}
else {
console.error("屏幕未关闭, 请尝试其他锁屏方案, 或等待屏幕自动关闭")
}
}



// ===================== ↓↓↓ 功能函数 ↓↓↓ =======================

function dateDigitToString(num){
return num < 10 ? '0' + num : num
}

function getCurrentTime(){
var currentDate = new Date()
var hours = dateDigitToString(currentDate.getHours())
var minute = dateDigitToString(currentDate.getMinutes())
var second = dateDigitToString(currentDate.getSeconds())
var formattedTimeString = hours + ':' + minute + ':' + second
return formattedTimeString
}

function getCurrentDate(){
var currentDate = new Date()
var year = dateDigitToString(currentDate.getFullYear())
var month = dateDigitToString(currentDate.getMonth() + 1)
var date = dateDigitToString(currentDate.getDate())
var week = currentDate.getDay()
var formattedDateString = year + '-' + month + '-' + date + '-' + WEEK_DAY[week]
return formattedDateString
}

// 通知过滤器
function filterNotification(bundleId, abstract, text) {
var check = PACKAGE_ID_WHITE_LIST.some(function(item) {return bundleId == item})
if (!NOTIFICATIONS_FILTER || check) {
console.verbose(bundleId)
console.verbose(abstract)
console.verbose(text)
console.verbose("---------------------------")
return true
}
else {
return false
}
}

// 保存本地数据
function setStorageData(name, key, value) {
const storage = storages.create(name) // 创建storage对象
storage.put(key, value)
}

// 读取本地数据
function getStorageData(name, key) {
const storage = storages.create(name)
if (storage.contains(key)) {
return storage.get(key, "")
}
// 默认返回undefined
}

// 删除本地数据
function delStorageData(name, key) {
const storage = storages.create(name)
if (storage.contains(key)) {
storage.remove(key)
}
}

// 获取应用版本号
function getPackageVersion(bundleId) {
importPackage(android.content)
var pckMan = context.getPackageManager()
var packageInfo = pckMan.getPackageInfo(bundleId, 0)
return packageInfo.versionName
}

// 屏幕是否为锁定状态
function isDeviceLocked() {
importClass(android.app.KeyguardManager)
importClass(android.content.Context)
var km = context.getSystemService(Context.KEYGUARD_SERVICE)
return km.isKeyguardLocked()
}

// 设置媒体和通知音量
function setVolume(volume) {
device.setMusicVolume(volume)
device.setNotificationVolume(volume)
console.verbose("媒体音量:" + device.getMusicVolume())
console.verbose("通知音量:" + device.getNotificationVolume())
}

📐 工具介绍

Auto.js

Auto.js 是利用安卓系统的 「无障碍服务」 实现类似于按键精灵一样,可以通过代码模拟一系列界面动作的辅助工具。

与 「按键精灵」 不同的是,它的模拟动作并不是简单的使用在界面定坐标点来实现,而是找控件来实现的。

免费版:Auto.js 4.1.1a Alpha2-armeabi-v7a-release

github:GitHub - hyb1996/Auto.js

官方文档:首页 - Auto.js

推荐使用VS Code 插件进行调试,调试完成后,还能通过此插件将脚本保存到手机上。

Tasker

Tasker 也是一个安卓自动化神器,与 Auto.js 结合使用可胜任日常工作流。

此处仅提供 Tasker 5.0 及以下的官方原版,原版不含正版验证,使用不受限制:

Tasker.4.9u4m.apk

Tasker.5.0u7m.apk

Tasker 定时打卡配置:

  1. 添加一个 「通知」 操作任务,通知标题修改为 「定时打卡」,通知文字随意,通知优先级设为 1。
  2. 添加两个配置文件,使用日期和时间作为条件,分别在上班前和下班后触发。

你也可以下载配置文件,导入到 Tasker 中使用,方法如下:

  1. 长按 菜单栏-任务,导入”发送通知.tsk.xml”。
  2. 长按 菜单栏-配置文件,导入”上班打卡.prf.xml” 和 “下班打卡.prf.xml”。
  3. 在任务编辑界面左下方有一个三角形的播放按钮,点击即可发送通知,方便调试。

🕹️ 使用方法

远程打卡

  • 向本机的 QQ 发送消息 「打卡」,或回复标题为 「打卡」 的邮件,或向 PushDeer 发送标题为「打卡」 的推送请求,即可触发打卡进程。
  • 向本机的 QQ 发送消息 「查询」,或回复标题为 「查询」 的邮件,或向 PushDeer 发送标题为「查询」 的推送请求,即可查询最新一次打卡结果。

暂停/恢复定时打卡

  • 向本机的 QQ 发送消息 「暂停」,或回复标题为 「暂停」 的邮件,或向 PushDeer 发送标题为「暂停」 的推送请求,即可暂停定时打卡功能(仅暂停定时打卡,不影响远程打卡功能)
  • 向本机的 QQ 发送消息 「恢复」,或回复标题为 「恢复」 的邮件,或向 PushDeer 发送标题为「恢复」 的推送请求,即可恢复定时打卡功能。

⚠️ 注意事项 (必读!!!)

  • AutoJs Pro 版本屏蔽了一些主流应用,如果要使用 QQ 作为回复方式,不要使用 AutoJs Pro 版!
  • 首次启动 AutoJs,需要为其开启无障碍权限。
  • 运行脚本前,请在 AutoJs 菜单栏中(从屏幕左边划出),开启 「通知读取权限」。
  • 若无法通过 app.launchPackage() 方法启动应用,请开启该应用的「自启动」「允许后台弹窗」。
  • AutoJs、Tasker 可息屏运行,需要在系统设置中开启通知亮屏。
  • 为保证 AutoJs、Tasker 进程不被系统清理,可调整它们的电池管理策略、加入管理应用的白名单,为其开启前台服务、添加应用锁…
  • 虽然脚本可执行完整的打卡步骤,但推荐开启钉钉的极速打卡功能,在钉钉启动时即可完成打卡,应把后续的步骤视为极速打卡失败后的保险措施。

📜 更新日志

2022-03-26

  1. 可以通过 PushDeer 接收通知、推送考勤结果

2022-03-01

  1. 可以通过Server酱来推送考勤结果

2021-10-23

  1. 适配网易邮箱大师7.0

2021-09-02

  1. 新增获取日志功能,发送 「日志」,可将运行日志作为邮件附件发送(最好使用内置邮件)
  2. 优化通知过滤器,过滤 Tasker 发出的无效通知

2021-07-07

  1. 登录流程自动同意隐私协议

2021-05-27

  1. 修改了部分常量的命名
  2. 移除了休息日不打卡的判断
  3. 在邮件的基础上,增加QQ作为新的通讯方式。除发送考勤结果需要手动指定应用外,使用QQ向本机发送 「查询、暂停、恢复」 指令,则会用QQ来回复查询或操作结果;使用邮件向本机发送指令则用邮件回复。

2021-05-06

  1. 增加音量上键监听,按下后中断所有子线程,也可以利用回调来进行调试
  2. 不再使用考勤机名称来判断连接状态
  3. 重新进入打卡界面前,先返回上级菜单,以解决顶号登录无法正常连接到考勤机的问题
  4. 启动钉钉时,将媒体音量和通知音量设为0

2021-03-15

  1. 运行时检查Auto.js版本,脚本需要在Auto.js 4.1.0及以上版本中运行
  2. 新增解锁是否成功的判断,若解锁失败则停止运行脚本
  3. 优化 signIn() 方法,使用 bundleId + activity 来判断登录情况
  4. 优化部分控件和信息的获取方式

2021-03-09

  1. 移除 「结束钉钉」、「检查更新」 这个两个过程,使用最近一次监测到的正在运行的应用的包名进行判断

  2. 补充一个万能锁屏方案:向Tasker发送广播,触发Tasker中的系统锁屏操作。

    • 在Tasker中添加一个任务,在任务中添加操作 「系统锁屏(关闭屏幕)」
    • 在Tasker中添加一个事件类型的配置文件,事件类别:系统-收到的意图
    • 在事件操作中填写:autojs.intent.action.LOCK_SCREEN ,保持发送方与接收方的action一致即可
1
2
3
app.sendBroadcast({
action: 'autojs.intent.action.LOCK_SCREEN'
});

2021-02-07

  1. 防止监听事件被耗时操作阻塞。

2021-01-15

  1. 移除 「进入工作台」 以及 「进入考勤打卡界面」 这两个过程
  2. 启动并成功登录钉钉后,直接使用intent拉起考勤打卡界面

2021-01-08

  1. 修复:通知过滤器报错

2020-12-30

  1. 优化:现在可以通过邮件来 暂停/恢复 定时打卡功能,以应对停工停产,或其他需要暂时停止定时打卡的特殊情况

2020-12-04

  1. 优化:打卡过程在子线程中执行,钉钉返回打卡结果后,直接中断子线程,减少无效操作

2020-10-27

  1. 修复:当钉钉的通知文本为null时,indexOf()方法无法正常执行

2020-09-24

  1. 优化:使用URL Scheme直接拉起考勤打卡界面
1
2
3
4
5
6
7
8
function attendKaoqin(){
var a = app.intent({
action: "VIEW",
data: "dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html"
});
app.startActivity(a);
sleep(5000)
}

获取URL的方式如下:

  1. 在PC端找到 「智能工作助理」 联系人
  2. 发送消息 “打卡” ,点击 「立即打卡」
  3. 弹出一个二维码。此二维码就是拉起考勤打卡界面的 URL,用自带的相机或其他应用扫描,并在浏览器中打开,即可获得完整URL
  4. 观察获取到的URL,找到 CorpId=xxxxxxxxxxxxxxxxxxx ,将CorpId的值填写到的脚本开头的CORP_ID这个常量中
  5. 仅使用 dingtalk://dingtalkclient/page/link?url=https://attend.dingtalk.com/attend/index.html,也可以拉起旧版打卡界面,钉钉会自动获取企业的CorpId。如果加入了多个组织,且没有填写CorpId,则在拉起考勤界面时会弹出一个选择组织的对话框。

2020-09-11

  1. 将上次考勤结果储存在本地
  2. 将运行日志储存在本地 /sdcard/脚本/Archive/
  3. 修复在下班极速打卡之后,重复打卡的问题

2020-09-04

  1. 将 “打卡” 与 “发送邮件” 分离成两个过程,打卡完成后,将钉钉返回的考勤结果作为邮件正文发送

2020-09-02

  1. 改为使用 “去打卡” 文本获取按钮。若找不到 “去打卡” 按钮,则直接点击 “考勤打卡” 的屏幕坐标

📢 声明

此仓库及脚本仅供学习交流,欢迎转载。旨在让人们关注996制度的存在和非法性,并尝试改变这种现象。

根据1994年第八届全国人大常委会通过和2018年第十三届全国人大常委会修正的《中华人民共和国劳动法》规定,劳动者每日工作时间不超过8小时,平均每周工作时间不超过44小时,而996工作制每周至少要工作72个小时,远超法律标准,因此996工作制度违反劳动法。

而钉钉却允许企业管理者违反法律,非法排班!

第三十六条 国家实行劳动者每日工作时间不超过八小时、平均每周工作时间不超过四十四小时的工时制度。

第四十一条 用人单位由于生产经营需要,经与工会和劳动者协商后可以延长工作时间,一般每日不得超过一小时;因特殊原因需要延长工作时间的,在保障劳动者身体健康的条件下延长工作时间每日不得超过三小时,但是每月不得超过三十六小时。

第四十四条 有下列情形之一的,用人单位应当按照下列标准支付高于劳动者正常工作时间工资的工资报酬:

(一)安排劳动者延长工作时间的,支付不低于工资的百分之一百五十的工资报酬;
(二)休息日安排劳动者工作又不能安排补休的,支付不低于工资的百分之二百的工资报酬;
(三)法定休假日安排劳动者工作的,支付不低于工资的百分之三百的工资报酬。

第九十条 用人单位违反本法规定,延长劳动者工作时间的,由劳动行政部门给予警告,责令改正,并可以处以罚款。

第九十一条 用人单位有下列侵害劳动者合法权益情形之一的,由劳动行政部门责令支付劳动者的工资报酬、经济补偿,并可以责令支付赔偿金:

(二)拒不支付劳动者延长工作时间工资报酬的;

相关项目:996 薪资计算助手


如果觉得还不错的话,就点击右上角, 给我个Star ⭐️ 鼓励一下我吧~

hexo 部署手册

Hexo 部署手册

Hexo搭建步骤

  1. 安装Git
  2. 安装Node.js
  3. 安装Hexo
  4. GitHub创建个人仓库
  5. 生成SSH添加到GitHub
  6. 将hexo部署到GitHub

1. 安装Git

1
sudo apt-get install git

安装好后,用git --version 来查看一下版本

2. 安装nodejs

1
2
sudo apt-get install nodejs
sudo apt-get install npm

检查一下有没有安装成功

1
2
node -v
npm -v

顺便说一下,windows在git安装完后,就可以直接使用git bash来敲命令行了,不用自带的cmd,cmd有点难用。

3. 安装hexo

1
npm install -g hexo-cli

依旧用hexo -v查看一下版本,至此就全部安装完了。

接下来初始化一下hexo

1
hexo init myblog

这个myblog可以自己取什么名字都行,然后

1
2
cd myblog //进入这个myblog文件夹
npm install

使用命令 hexo server 可缩写hexo s启动服务

1
$ hexo server

部署项目到Github远程仓库

  • 修改配置文件 _config.yml
1
2
3
4
5
6
# Deployment
## Docs: https://hexo.io/docs/one-command-deployment
deploy:
type: git
repo: git@github.com:2757412961/2757412961.github.io.git
branch: web
  • 操作命令
1
2
3
4
5
6
$ npm install hexo-deployer-git --save  #安装部署工具
$ hexo clean #清除缓存 可缩写hexo c
$ hexo generate #生成静态文件 可缩写hexo g
$ hexo deploy #部署到Github 可缩写hexo d
# 操作合集
hexo clean && hexo generate && hexo deploy

4. GitHub创建个人仓库

登录Github新建一个仓库,仓库名必须为你的Github用户名.github.io 例如: 我的用户名是:Lete 那么格式因该为:lete.github.io

image-20220903214114617

远程仓库开启 github pages,指定分支为上述提到的web

image-20220905104750608

5. 生成SSH添加到GitHub

安装成功后,将 git 与 GitHub 账号绑定,右键打开 Git Bash,然后设置配置信息:

1
2
3
# 配置用户名和邮箱
git config --global user.name "github 用户名"
git config --global user.email "github 注册邮箱"

比如我的配置就是:

1
2
git config --global user.name "2757412961"
git config --global user.email "2757412961@qq.com"

接着生成 ssh 密钥文件,输入如下命令后直接三次回车即可,一般不需要设置密码;

1
2
# 生成 ssh 密钥
ssh-keygen -t rsa -C "github 注册邮箱"

我生成秘钥的命令:

1
ssh-keygen -t rsa -C "2757412961@qq.com"

img

一般执行上述命令之后,会生成 id_rsaid_rsa.pub 两个文件,前者是我们私有的,而后者则是对外开放的。接着找到生成的 .ssh 的文件夹中的 id_rsa.pub 密钥,将内容复制;

image-20220903214402850

然后打开 GitHub-Settings-Keys 页面,创建一个新的 SSH key,填写 TitleKeyTitle 可以随意,而 Key 的内容则是我们刚才复制的 id_rsa.pub 中的内容,最后点击 Add SSH key 即可;

img

6. 将hexo部署到GitHub(手动)

这一步,我们就可以将hexo和GitHub关联起来,也就是将hexo生成的文章部署到GitHub上,打开站点配置文件 _config.yml,翻到最后,修改为 YourgithubName就是你的GitHub账户

1
2
3
4
5
6
# Deployment
## Docs: https://hexo.io/docs/one-command-deployment
deploy:
type: git
repo: git@github.com:2757412961/2757412961.github.io.git
branch: web

然后

1
hexo clean && hexo generate && hexo deploy

7.GitHub Actions(自动)

1
就是DevOps,可以理解成 GitHub 通过一些流水线的配置(CI/CD),然后在本地推送代码的时候触发流水线执行,自动部署站点。

7.1 新建 .github/workflows/pages.yml 文件

修改

1
2
3
4
5
6
7
8
9
10
11
# 触发器、分支
on:
push:
branches:
- main # default branch

...

deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
user_name: 2757412961
user_email: 2757412961@qq.com
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
name: Pages

# 触发器、分支
on:
push:
branches:
- main # default branch
jobs:
# 子任务
pages:
runs-on: ubuntu-latest # 定运行所需要的虚拟机环境
permissions:
contents: write
steps:
- uses: actions/checkout@v2
# with:
# submodules: true
# fetch-depth: 0
# 每个name表示一个步骤:step
- name: Use Node.js 16.x
uses: actions/setup-node@v2
with:
node-version: '16.14.1' # 自己正在使用的node版本即可
# - run: node -v # 查看node版本号
# 缓存依赖项: https://docs.github.com/cn/actions/using-workflows/caching-dependencies-to-speed-up-workflows
- name: Cache NPM dependencies
uses: actions/cache@v2
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
# path: node_modules
key: ${{ runner.OS }}-npm-cache
restore-keys: |
${{ runner.OS }}-npm-cache
# 查看路径 : /home/runner/work/blog/blog
# - name: Look Path
# run: pwd
# 查看文件
- name: Look Dir List
run: tree -L 3 -a
# 第一次或者依赖发生变化的时候执行 Install Dependencies,其它构建的时候不需要这一步
- name: Install Dependencies
run: npm install
- name: Look Dir List
run: tree -L 3 -a
# - name: clean theme cache
# run: git rm -f --cached themes/tenacity
# run: git submodule deinit themes/tenacity && git rm themes/tenacity
# 安装主题
- name: Install Theme
run: git submodule add https://github.com/all-smile/tenacity.git themes/tenacity
- name: Clean
run: npm run clean
- name: Build
run: npm run build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
user_name: 2757412961
user_email: 2757412961@qq.com
# 获取提交文章源码时的commit message,作为发布gh-pages分支的信息
commit_message: ${{ github.event.head_commit.message }}
full_commit_message: ${{ github.event.head_commit.message }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# GITHUB_TOKEN不是个人访问令牌,GitHub Actions 运行器会自动创建一个GITHUB_TOKEN密钥以在您的工作流程中进行身份验证。因此,您无需任何配置即可立即开始部​​署
publish_dir: ./public
allow_empty_commit: true # 允许空提交
# Use the output from the `deploy` step(use for test action)
- name: Get the output
run: |
echo "${{ steps.deploy.outputs.notify }}"

7.2 修改 _config.yml 文件中的Deploy配置

1
2
3
4
5
6
7
8
# Deployment
## Docs: https://hexo.io/docs/one-command-deployment
deploy:
type: git
repo: git@github.com:2757412961/2757412961.github.io.git
branch: web
# 默认提交信息: Site updated: {{ now('YYYY-MM-DD HH:mm:ss') }}
message: ${{ github.event.head_commit.message }} # 直接将提交消息传输到 GitHub Pages 存储库

7.3 设置 deploy_key

拿到第5步中生成的密钥。

  • id_rsa 私钥
  • id_rsa.pub 公钥

转到Deploy Keys并使用Allow write access添加您的公钥id_rsa.pub,name写为HEXO_ACTIONS_DEPLOY_KEY,指定用途,方便后面维护

image-20220905113340582

转到Actions secrets并将您的私钥 id_rsa 添加为 ACTIONS_DEPLOY_KEY(这个名称在yml文件中需要使用)

image-20220905111813096

7.4 启动 Git Action

本地仓库直接push,触发 GitHub Actions 自动构建发布

image-20220905112116599

7.5 非法输入值

pages.yml 文件的 Deploy 步骤下,发布的时候需要一些参数配置,这些参数名是指定好的,不可以随便写,比如 commit_msg应该使用 commit_message

Warning: Unexpected input(s) ‘commit_msg’, valid inputs are [‘deploy_key’, ‘github_token’, ‘personal_token’, ‘publish_branch’, ‘publish_dir’, ‘destination_dir’, ‘external_repository’, ‘allow_empty_commit’, ‘keep_files’, ‘force_orphan’, ‘user_name’, ‘user_email’, ‘commit_message’, ‘full_commit_message’, ‘tag_name’, ‘tag_message’, ‘enable_jekyll’, ‘disable_nojekyll’, ‘cname’, ‘exclude_assets’]

img

8 参考文献

https://www.cnblogs.com/all-smile/p/16608503.html


java 动态代理

[TOC]

一、代理模式简介

1.1、代理模式

提供了间接对目标对象进行访问的方式,即通过代理对象访问目标对象。

1.2、代理模式的好处

在实现目标对象功能的基础上,增加额外的功能,扩充目标对象的功能。符合开闭原则:即不改变既有代码的前提下,对功能进行扩展。

例如,需要在所有类的方法执行前后打印日志。

二、JVM 创建对象

对象创建过程


对象创建过程

关于 Class 对象和实例对象

Class 对象:每个类运行时的类型信息由 Class 对象来表示,Class 对象中包含与这个类有关的信息。

实例对象:实例对象是由 Class 对象创建的。


Class 对象和实例对象

Java 程序在开始运行之前并非被完全加载,其各个类都是在必需时才加载的 —— 懒加载

获取 Class 对象的方式

Class.forName(“类的全限定名”)

实例对象.getClass()

类名.class —— 类字面常量

如:

Class clazz = int.class

如果字段被 static final 修饰,称为“编译时常量”。调用该字段时,不会对类进行初始化。

因为这个字段在编译期就把结果放入常量池中了。

三、静态代理

静态代理的逻辑:


静态代理的逻辑

静态代理缺陷

对于每一个目标类都需要写对应的 XXProxy 代理类,如果目标类很多的话,写成百上千个代理类是不切实际的。

四、动态代理

动态代理的思路


动态代理的思路

动态代理的优势

在程序运行时,JVM 才对被代理对象(目标对象)生成代理对象。涉及到反射。

注意,动态代理的时候目标对象必须得实现接口,否则动态代理无法使用,原因很明显,因为代理对象本身不用实现接口,它用到的是目标对象的引用,所以如果目标对象不实现接口的话,根本没法进行代理。

如何从 interface 创建 Class 对象?

JDK 提供:java.lang.reflect.InvocationHandler接口 + java.lang.reflect.Proxy

Proxy 类有一个静态方法:

1
public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)

入参是 ClassLoader 类加载器以及一组接口 Class 对象。出参是 Class 类型,也就是一个 Class 对象,实际就是一个代理 Class,通过这个 Class 可以创建对应入参的代理对象。


动态代理的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.lang.reflect.*;

public class DynamicProxy {
public static void main(String[] args) throws Throwable {
// 通过 Dog 接口创建了一个代理 Class
Class proxyClass = Proxy.getProxyClass(Dog.class.getClassLoader(), Dog.class);
Constructor constructor = proxyClass.getConstructor(InvocationHandler.class);

// 用构造器创建代理实例对象,需要传入 InvocationHandler
Dog dog = (Dog)constructor.newInstance(new InvocationHandler() {

// 调用代理对象的方法,都会调用 invoke()
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 代理需要目标对象的引用
// 手动创建目标对象,硬编码,不推荐
// 推荐的做法是,将目标对象作为参数传进来
WhiteDog whiteDog = new WhiteDog();
Object result = method.invoke(whiteDog, args);
return result;
}
});

dog.canRun();
dog.canBark();
}
}

// 重构之后
import java.lang.reflect.*;

public class DynamicProxy {

public static void main(String[] args) throws Throwable {
WhiteDog target = new WhiteDog();
Dog dog = (Dog) getProxy(target);

dog.canRun();
dog.canBark();
}

// 传入目标对象的引用 target
private static Object getProxy(final Object target) throws Throwable {
Class proxyClass = Proxy.getProxyClass(Dog.class.getClassLoader(), Dog.class);
Constructor constructor = proxyClass.getConstructor(InvocationHandler.class);
// 入参是目标对象 target, 出参是代理对象 proxy, 类型是 Object
return constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(target, args);
}
});
}
}

实际使用的是 Proxy.newProxyInstance()

1
2
3
4
5
6
7
8
9
10
11
12
13
// 传入目标对象的引用 target  
private static Object getProxy2(final Object target) throws Throwable{
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
target.getClass().getInterfaces(), // 让代理实例对象和原对象实现同一个接口
// 这么写其实是 匿名内部类 的写法
new InvocationHandler() { // 代理对象的方法最终会导向 invoke 方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(target,args);
}
});
}

动态代理逻辑图


动态代理逻辑图

五、Cglib 代理

Cglib 代理的实现

前面说的动态代理需要目标类实现接口,但是如果某个类根本不实现接口怎么办呢?这时候就需要 Cglib 代理了,也称 子类代理

逻辑是在内存中构建一个子类对象集成目标对象,实现目标对象的功能扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibProxy {

public static void main(String[] args) {
// 目标: 通过 Cglib 代理 BlackDog
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(BlackDog.class);

enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 此处一定要使用 proxy 的 invokeSuper 方法来调用目标类的方法
return methodProxy.invokeSuper(o, objects);
}
});
// 生成代理实例
BlackDog dog = (BlackDog)enhancer.create();
dog.canBark();
dog.canRun();
}

}

Cglib 代理总结

在 Spring 的 AOP 编程中: 面向切面编程

如果加入容器的目标对象有实现接口, 用 JDK 的动态代理

如果目标对象没有实现接口,用 Cglib 代理

六、参考材料

Java 动态代理作用是什么?

Java中三种代理模式

Java中Class对象详解


云原生概念和技术路线

目录

目录 1
一、 调研方向 2
二、 云原生的概念 3
2.1 云原生的诞生 3
2.2 云原生的发展历程 3
2.3 Pivotal的云原生定义 4
2.4 Gartner的云原生定义 5
2.5 CNCF的云原生定义 5
2.6 云原生定义概括 5
三、 CNCF 7
3.1 云原生计算基金会CNCF 7
3.2 CNCF云原生路线图(Trail Map) 7
3.3 CNCF云原生全景图(Trail Map) 8
四、 相关技术及发展趋势 10
4.1 CNCF云原生全景图详解 10
4.1.1 供应层 (Provisioning) 10
4.1.2 运行时层(Runtime) 10
4.1.3 编排和管理层(Orchestration and Management) 10
4.1.4 应用定义和开发层 (Application Definition and Developement) 11
4.1.5 贯穿所有层的工具 11
4.1.6 可观察性和分析(Observability and Analysis) 12
4.1.7 平台类(Platform) 12
4.1.8 小结 12
4.2 云原生核心概念 12
4.2.1 DevOps与CI/CD 13
4.2.2 微服务、API管理与集成 14
4.2.3 容器与Docker 14
4.2.4 Kubernetes与容器编排之战 15
4.3 发展趋势和未来展望 15
4.3.1 机遇和挑战 15
4.3.2 云原生2.0(华为) 18
4.3.3 未来发展趋势 18
五、 参考链接 20

1 调研方向

梳理云原生的概念及相关技术和发展趋势。

https://landscape.cncf.io/
图 一.1CNCF云原生全景图(详)

2 云原生的概念

1.

1.

2.1 云原生的诞生

随着虚拟化技术的成熟和分布式框架的普及,在容器技术、可持续交付、编排系统等开源社区的推动下,以及微服务等开发理念的带动下,应用上云已经是不可逆转的趋势。

图 二.1云原生的发展史,来自CNCF基金会执行董事Dan Kohn
云计算的3层划分,即基础设施即服务(IaaS)、平台即服务(PaaS)、软件即服务(SaaS)为云原生提供了技术基础和方向指引,真正的云化不仅仅是基础设施和平台的变化,应用也需要做出改变,摈弃传统的土方法,在架构设计、开发方式、部署维护等各个阶段和方面都基于云的特点,重新设计,从而建设全新的云化的应用,即云原生应用

2.2 云原生的发展历程

云原生(Cloud Native)最初来描述云上应用的典型架构与特性,随着容器、kubernetes、Serverless、FaaS技术的演进,CNCF(Cloud Native Computing Foundation ,云原生计算基金会)把云原生的概念更广泛地定义为“让应用更有弹性、容错性、观测性的基础技术,让应用更容易部署、管理的基础软件、让应用更容易编写、编排的运行框架等”,希望能够让开发者最好的利用云的资源、产品和交付能力。
下边大致梳理一下云原生的发展过程。

  • 2004 年 ~ 2007 年,Google 已在内部大规模地使用像 Cgroups 这样的容器技术;
  • 2008 年,Google 将 Cgroups 合并进入了 Linux 内核主干。
  • 2013 年,Docker 项目正式发布。
  • 2014 年,Kubernetes 项目也正式发布。
  • 2015 年,CNCF (Cloud Native Computing Foundation)云原生基金会成立,CNCF 是目前云计算领域最成功的开源基金会之一,是 Kubernetes、 etcd、Envoy 等知名开源项目的托管基金会。
  • 2017 年,CNCF 达到 170 个成员和 14 个基金项目。
  • 2018 年,CNCF 成立三周年有了 195 个成员,19 个基金会项目和 11 个孵化项目,如此之快的发展速度在整个云计算领域都是非常罕见的。

2.3 Pivotal的云原生定义

云原生(Cloud Native)这个概念,是由Pivotal的Matt Stine于2013年首次提出,他还在2015年出版了《Migrating to Cloud-Native Application Architectures(迁移到云原生架构)》一书。
Pivotal作为云原生(Cloud Native)应用架构中先驱者和探路者,于2015年提出了云原生应用。同一年Google主导成立了云原生计算基金会(CNCF),起初CNCF对云原生的定义包含三个方面:应用容器化、面向微服务架构、应用支持容器的编排调度。
随着科技的发展,CNCF基金会对云原生进行了重新定义:云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API
上面提到云原生的代表技术包括容器、服务网格(Service Mesh)、微服务、不可变基础设施和声明式API。另外一种比较主流的说法是云原生=微服务+DevOps+持续交付+容器化
Pivotal最新官网对云原生概括为4个要点:DevOps+持续交付+微服务+容器。

2.4 Gartner的云原生定义

2019年,Gartner曾经发布报告表示云原生时代已经到来,在未来三年中将有75%的全球化企业将在生产中使用容器化的应用。
但Gartner提到云原生的定义尚不明确,却含义丰富。云原生对于不同的人和组织来讲,有着不同的理解。众多顶级技术的铸造者、Matt Stine的东家Pivotal如此定义云原生。
“Cloud native is an approach to building and running applications that fully exploit the advantages of the cloud computing model.”–云原生是一种构建和运行充分利用云计算模型优势的应用程序的方法。

2.5 CNCF的云原生定义

CNCF云原生计算基金会如此定义云原生:
“云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格(Service Mesh)、微服务、不可变基础设施和声明式API。这些技术能够构建容错性好、易于管理和便于观察的松耦合系统。结合可靠的自动化手段,云原生技术使工程师能够轻松地对系统作出频繁和可预测的重大变更。”

2.6 云原生定义概括

云原生是指从云的原生应用角度出发,一整套设计、开发、部署、运行、维护的流程、技术栈以及背后文化理念的统称。
云原生简单来说,摈弃传统的土方法,在架构设计、开发方式、部署维护等各个阶段和方面都基于云的特点,重新设计,从而建设全新的云化的应用,即云原生应用

3 CNCF

1.

3.1 云原生计算基金会CNCF

提到云原生,就不能不介绍云原生计算基金会CNCF(Cloud Native Computing Foundation)。CNCF于2015 年7月由Google 牵头成立,隶属于 Linux 基金会,初衷是围绕云原生服务云计算,致力于培育和维护一个厂商中立的开源生态系统,维护和集成开源技术,支持编排容器化微服务架构应用,通过将最前沿的模式民主化,让这些创新为大众所用。
CNCF的使命包括以下三点:
• 容器化包装
• 通过中心编排系统的动态资源管理
• 面向微服务
全球主流的科技企业和云计算厂商绝大部分都是CNCF会员,其中不乏多家来自中国的科技巨头。

图 二.2 CNCF黄金、白金会员

3.2 CNCF云原生路线图(Trail Map)

对于企业在复杂的基础架构之上如何推动云原生应用的更好落地,从而更好地适应环境与业务的发展,CNCF给出了路线图(Trail Map)用于对用户在整体上给出指导建议,共分成十个步骤(容器化;CI/CD;应用定义及编排;监控及分析;服务代理、发现和网格;网络、策略及安全;分布式数据库及存储;流与消息;镜像库与运行时;软件分发)进行实施,而在不同的步骤都可以结合CNCF全景图(Landscape)中列出的产品或服务进行选择。

3.3 CNCF云原生全景图(Trail Map)

CNCF全景图则列举了和云原生相关的产品及服务的完整名单,这1381个项目共同构成了恢弘庞大的云原生世界。整个全景图按照功能分为29个模块,分别归属于9种大的类别(应用定义与开发、编排与管理、运行时、配置、平台、可观察性与分析、Serverless、会员和其它)。值得注意的是其中专门有一种分类是Cards from China,列举了来自中国的145个项目,其中不乏许多大家耳熟能详的知名项目。
从CNCF的理念及野心来看,基于云原生的基础设施正在壮大和蚕食非云的市场,未来极有可能成为整个IT生态事实上的意见领袖和领导者。

4 相关技术及发展趋势

1.

1.

4.1 CNCF云原生全景图详解

首先,我们剥离掉所有单个的技术,仅查看类别(如下图)。图中有不同的“行”,像建筑的不同层,每层都有自己的子类别。最底层提供了构建云原生基础设施的工具。往上,你可以开始添加运行和管理应用程序所需的工具,比如运行时和调度层。在最上层,有定义和开发应用程序的工具,比如数据库、镜像构建和 CI/CD 工具(我们将在后文讨论)。
云原生全景图始于基础设施,往上的每一层都更接近实际的应用程序。这就是每层代表的意思(后面我们会讨论上图右边的两“列”)。下面我们就从最底层开始,逐层进行解析。

4.1.1 供应层 (Provisioning)

供应指的是为云原生应用准备标准基础环境所涉及的工具。它包含了基础设施的创建、管理、配置流程的自动化,以及容器镜像的扫描、签名和存储等。供应层通过提供设置和实施策略,在应用程序和平台中构建身份验证和授权,以及处理密钥分发等等的工具,也拓展到了安全领域。

  • 自动化和部署工具:帮助工程师在无需人工干预情况下即可构建计算环境;
  • 容器注册表:存储应用程序的可执行文件;
  • 不同安全领域的安全和合规框架;密钥管理解决方案:通过加密确保只有授权的用户才能访问特定的应用程序。
  • 这些工具使工程师可以编写基础设施参数,使系统可以按需搭建新环境,确保了一致性和安全性。

4.1.2 运行时层(Runtime)

像很多 IT 术语一样,运行时没有严格的定义,且可以根据语境有不同的用法。狭义上讲,运行时是特定机器上准备运行应用程序的沙盒——也就是保障应用程序正常运行所需的最低配置。广义上讲,运行时是运行一个应用程序所需的所有工具。在 CNCF 云原生全景图中,运行时保障了容器化应用程序组件的运行和通信。

  • 云原生存储:为容器化应用提供虚拟磁盘或持久化存储;
  • 容器运行时:为容器提供隔离、资源和安全;
  • 云网络:分布式系统的节点(机器或进程)通过其连接和通信。

4.1.3 编排和管理层(Orchestration and Management)

一旦按照安全和合规性标准(供应层)自动化基础设施供应,并安装了应用程序运行所需的工具(运行时层),工程师就需要弄清楚如何编排和管理应用程序。编排和管理层将所有容器化服务(应用程序组件)作为一个群组管理。这些容器化服务需要相互识别和通信,并需要进行协调。这一层可为云原生应用提供自动化和弹性能力,使云原生应用天然具有可扩展性。

  • 编排和调度:部署和管理容器集群,确保它们具有弹性伸缩能力,相互之间低耦合,并且可扩展。事实上,编排工具(绝大多数情况下就是 Kubernetes)通过管理容器和操作环境构成了集群;
  • 协调和服务发现:使得服务(应用程序组件)之间可以相互定位和通信;
  • 远程进程调用(RPC):使跨节点服务间通信的技术;
  • 服务代理:服务间通信的中介。服务代理的唯一目的就是对服务之间的通信进行更多控制,而不会对通信本身添加任何内容。服务代理对下面将提到的服务网格(Service Mesh)至关重要。
  • API 网关:一个抽象层,外部应用可通过 API 网关进行通信;
  • Service Mesh:某种程度上类似于 API 网关,它是应用程序进行通信的专用基础架构层,提供基于策略的内部服务间通信。此外,它还可能包含流量加密、服务发现、应用程序监控等内容。

4.1.4 应用定义和开发层 (Application Definition and Developement)

我们来到了最顶层。应用定义和开发层,顾名思义,聚集了让工程师构建和运行应用程序的工具。上述所有内容都是关于构建可靠、安全的环境,以及提供全部所需的应用程序依赖。

  • 数据库:使应用程序能以有序的方式收集数据;
  • 流和消息传递:使应用程序能发送和接收消息(事件和流)。它不是网络层,而是让消息成为队列并处理消息的工具;
  • 应用程序定义和镜像构建:用于配置、维护和运行容器镜像(应用程序的可执行文件)的服务;
  • 持续集成和持续交付(CI/CD):使开发者可自动测试代码是否与代码库(应用程序的其余部分)兼容。如果团队足够成熟,甚至可以自动部署代码到生产环境。

4.1.5 贯穿所有层的工具

接下来我们将进入到云原生全景图右侧贯穿所有层的两列。可观察性和分析(Observability&analysis)是监控各层的工具,平台(Platforms)则将各层中不同的技术捆绑为一个解决方案。

4.1.6 可观察性和分析(Observability and Analysis)

为了限制服务中断并降低解决问题的平均时间(MRRT),你需要监控和分析应用层序的方方面面,以便在出现异常时可立即发现并纠正。复杂环境中容易出现故障,这些工具可快速识别并解决故障,从而降低故障带来的影响。由于这一类别贯穿并监控各层,因此它在侧面,而不是嵌入到某一层中。

  • 日志工具:收集事件日志(有关进程的信息);
  • 监控方案:收集指标(以数字表示的系统参数,例如 RAM 可用性);
  • 追踪工具:追踪比监控更进了一步,它们监控用户请求的传播,与服务网格相关。
  • 混沌工程(Chaos Engineering):在生产环境中测试软件的工具,可识别缺陷并进行修复,减少其对服务交付的影响。

4.1.7 平台类(Platform)

仅有存储并不能提供应用程序所需的全部功能。你还需要编排工具,容器运行时,服务发现,网络,API 网关等等。平台覆盖多层,将不同的工具组合在一起,以解决更大的问题。
配置和微调不同的模块使其安全可靠,并确保它利用的技术都能及时更新、所有漏洞都打了补丁,这并不是一件容易的事情。使用平台时,用户不用额外担心这些细节问题。
你可能会注意到,所有的类别都围绕着 Kubernetes 展开。这是因为 Kubernetes 虽然只是云原生景观图这张拼图中的一块,但它却是云原生技术栈的核心。顺便说一下,CNCF 刚创建时,Kubernetes 就是其中的第一个种子项目,后来才有了其他项目。

  • Kubernetes 发行版:采用未经修改的开放源代码(尽管有人对其进行了修改),并根据市场需要增加了其他功能;
  • 托管的 Kubernetes:类似于 Kubernetes 发行版,但是由提供商托管;
  • Kubernetes 安装程序:自动执行 Kubernetes 的安装和配置过程;
  • PaaS/容器服务:类似于托管的 Kubernetes,但是包含了一套更广泛的应用部署工具(通常是来自云原生景观图)。

4.1.8 小结

在每个类别中,针对相同或相似的问题,都有不同的工具可选择。有一些是适用于新现实的预云原生技术,还有一些则是全新的。区别在于它们的实现和设计方法。没有完美的技术符合你的所有需求。大多数情况下,技术受设计和架构选择的限制——始终需要权衡取舍。
在选择技术栈时,工程师必须仔细考虑每种能力和需要权衡取舍的地方,以确定最合适的选项。虽然这样会让情况变得更复杂,但在选择应用程序所需的最适合的数据存储、基础设施管理、消息系统等方案时,这样做是最可行的办法。现在,构建一个系统比云原生之前的时代容易多了。如果构建恰当,云原生技术将提供更强大的灵活性。在现如今快速变化的技术生态中,这可能是最重要的能力之一。
详细介绍云原生全景图的每一层,见 https://cloud.tencent.com/developer/article/1851558

4.2 云原生核心概念

下面的表格里代表性的列举了云原生技术层的几个领域及相关项目。

4.2.1 DevOps与CI/CD

DevOps
DevOps(Development & Operations,开发和运维)是09年提出来的概念,但一直没有太火。直到14年,容器与微服务架构的提出,DevOps才得到了快速的发展。DevOps不单是一个实现自动化的工具链,而是组织、流程与技术的结合。组织上强调全栈团队、团队特性专一、团队自治;技术上打通开发与运维;流程上强调端到端、可视化、灰度升级、A/B测试等。
持续集成
持续集成(CONTINUOUS INTEGRATION,CI)指的是开发人员频繁的(一天多次的)将所有开发者的工作合并到主干上。这些新提交在最终合并到主线之前,都需要通过编译和自动化测试流进行验证,以保障所有的提交在合并主干之后的质量问题,对可能出现的一些问题进行预警。持续集成的核心在于确保新增的代码能够与原先代码正确的集成。
持续交付
与持续集成相比,持续交付(CONTINUOUS DELIVERY,CD)的侧重点在于交付,其核心对象不在于代码,而在于可交付的产物。由于持续集成仅仅针对于新旧代码的集成过程执行了一定的测试,其变动到持续交付后还需要一些额外的流程。与持续集成相比较,持续交付添加了测试Test->模拟Staging->生产Production的流程,也就是为新增的代码添加了一个保证:确保新增的代码在生产环境中是可用的。
持续部署
持续部署(CONTINUOUS DEPLOYMENT)指的是通过自动化部署的手段将软件功能频繁的进行交付。与持续交付以及持续集成相比,持续部署强调了通过自动部署的手段,对新的软件功能进行集成。同持续交付相比持续集成的区别体现在对生产的自动化。从开发人员提交代码到编译、测试、部署的全流程不需要人工的干预,完全通过自动化的方式执行。这一策略加快了代码提交到功能上线的速度,保证新的功能能够第一时间部署到生产环境并被使用。

4.2.2 微服务、API管理与集成

微服务
微服务(Microservice)概念最早出现于2012年,2015年以后受到越来越多的关注,并且逐渐开始流行开来。其中著名技术大神Martin Fowler功不可没,他于2014年发表的一篇博客《Microservices: a definition of this new architectural term》(微服务:新技术架构的定义)清晰的定义和阐述了微服务概念。
微服务架构将单体应用,按照业务领域拆分为多个高内聚低耦合的小型服务,每个服务运行在独立进程,由不同的团队开发和维护,服务间采用轻量级通信机制,如HTTP RESTful API,独立自动部署,可以采用不同的语言及存储方式。微服务体现去中心化、天然分布式,是中台战略落地到IT系统的具体实现方式的技术架构,用来解决企业业务快速发展与创新时面临的系统弹性可扩展、敏捷迭代、技术驱动业务创新等难题。
API管理与API集成
微服务相关的两个具体领域,API管理与API集成。

  • 全生命周期API管理
  • API网关:微服务基础设施
  • Kong:API网关独角兽
  • RapidAPI:全球最大API市场
  • Mulesoft:API集成/iPaaS/API管理领头羊

微服务2.0:服务网格与Serverless
微服务2.0的服务网格(Service Mesh)应运而生。服务网格这个词最早由著名开源服务网格项目Linkerd所在的Buoyant公司CEO William Morgan所提出。按照他的定义,服务网格是一个软件基础设施层,用于控制和监视微服务应用程序中的内部、服务到服务的流量。
Serverless是一种构建和管理基于微服务架构的完整流程,它与传统架构的不同之处在于,完全由第三方管理,由事件触发,存在于无状态、暂存的计算容器内。Serverless相关的重要概念包括FaaS(Functions as a Service,函数即服务)。开发者把函数上传到云厂商的FaaS平台,函数只在被请求时才实例化运行,然后被销毁,其它时候不占用任何服务器资源,完全实现按需使用,大幅度降低了服务器占用和成本。

4.2.3 容器与Docker

虚拟化与容器
虚拟机虽然可以隔离出很多“子电脑”,但占用空间大,启动慢,虚拟机软件可能还要花钱(例如VMware)。而容器技术恰好没有这些缺点,它不需要虚拟出整个操作系统,只需要虚拟一个小规模的环境(类似“沙箱”),启动时间很快,几秒钟就能完成。而且,它对资源的利用率很高(一台主机可以同时运行几千个Docker容器)。此外它占的空间很小,虚拟机一般要几GB到几十GB的空间,而容器只需要MB级甚至KB级。虚拟机和以Docker为代表的容器都是虚拟化技术,不过容器属于轻量级的虚拟化。
Docker
而Docker与传统的Linux容器也并不完全一致。Docker技术最初是建立在LXC技术之上的,大多数人都把LXC技术与传统的Linux容器联系在一起,尽管后来它已经摆脱了这种依赖性。LXC作为轻量级虚拟化很有用,但它没有很好的开发人员或用户体验。Docker技术带来的不仅仅是运行容器的能力,它还简化了创建和构建容器、加载镜像和镜像版本控制等过程。传统的Linux容器使用可以管理多个进程的init系统,这意味着整个应用可以作为一个整体运行。Docker鼓励将应用程序分解为它们各自的进程,并提供了实现这一点的工具,这种粒度有不少优点。

4.2.4 Kubernetes与容器编排之战

容器编排与Kubernetes
在单机上运行容器,无法发挥它的最大效能,只有形成集群,才能最大程度发挥容器的良好隔离、资源分配与编排管理的优势。所以企业需要一套管理系统,对Docker及容器进行更高级更灵活的管理,按照用户的意愿和整个系统的规则,完全自动化的处理好容器之间的各种关系,这叫做编排(Orchestration)。
Kubernetes是基于Docker的开源容器集群管理系统,为容器化的应用提供资源调度、部署运行、服务发现、扩容缩容等整一套功能,因为容器本身可移植,所以Kubernetes容器集群能跑在私有云、公有云或者混合云上。

4.3 发展趋势和未来展望

4.3.1 机遇和挑战

机遇
每一次IT产业架构的变革都会带来巨大的机遇和行业洗牌的挑战。过去的三四十年间,IT业经历了多次重大的变革,包括20世纪七八十年代从大型机向小型机的转移、九十年代C/S架构的普及,以及21世纪初互联网的兴起,先后造就了IBM、思科、惠普、Oracle、EMC、SAP等巨头企业。
历次IT技术革命还有个共同特点:无论原有的基础软硬件公司此前有多么牢不可破的垄断地位,一旦不能符合新的IT技术变革的趋势,洗牌在所难免。
而云原生是一种理念和架构,用于以针对云环境优化的方式组装上述所有基于云的组件。因此云原生也是一个目的地:对于那些希望实现基础设施和流程现代化,甚至组织文化现代化的企业来说,最终的目标是仔细选择最适合其具体情况的云技术。
发展趋势
从统计数据和发展趋势来看,云原生被接受的程度和普及速度正在大大加快,例如下图显示,自从2016年以来容器的使用量每年都在快速上升。IDC预计,到2022年90%的应用程序将采用微服务架构和第三方代码,35%的生产应用程序将诞生于云端。由于容器和敏捷方法的采用,预计2018-2023年间将诞生5亿个新应用程序。由数字化转型,以及接受和采用新技术的需求驱动,云原生将更深入地渗透到大型企业组织中。这意味着云原生技术和方法可能会遵循敏捷和DevOps的模式,越来越多地吸引更多的利益相关者,包括管理者和业务线领导人,在未来几年内覆盖一半或更多的组织。

云原生安全领域
据CNCF统计,采用容器技术的挑战中,开发团队面临的文化挑战、安全性、复杂性、就绪性和监控分别排在前五位。

在云原生架构中,安全问题显得尤其突出的原因有以下几点:
1、快速迁移到云原生架构对企业安全状况和运营产生了深远的影响。在容器、微服务和编排框架的世界中,以持久“状态”运行在“服务器”上的“应用程序”的概念已经过时。现在,该应用程序或服务是一个分布式系统,由多个组件组成,这些组件运行在数量可变的节点上,处于几乎恒定的变化状态。依赖于机器隔离和可预测的系统状态的传统安全控制是无效的。对服务到服务的通信视而不见的安全策略以及缺乏水平可扩展的控件,根本无法跟上当今微服务应用程序的步伐。
2、随着企业将工作负载从数据中心转移到AWS、Google Cloud Platform和Microsoft Azure,它们已经改变了购买安全性的方式。他们需要独立于平台的安全工具,这样就不会被绑定到特定的云平台中。
3、复杂系统可以创建大量的警报和事件日志,这会是一项惊人的任务。安全项目被堆积如山的繁忙工作所淹没,分析师们疲惫不堪。随着分析师对惊人的数据量变得不敏感,真正的问题就从他们的手指间溜走了。
4、DevOps是一种协作方法,它将开发人员和IT操作统一起来,以加快应用程序的构建、测试和部署,它也影响了IT安全。当开发人员可以直接将他们的应用程序部署到生产服务器上,因为业务敏捷性需要它时,他们就不能停下来找出安全问题。DevOps提供了一种完全不同的安全方式,安全自动化有很多机会。
和云原生安全相关的初创企业
1、Capsule8(B轮)
2、Aqua Security(C轮)
3、Twistlock(被收购)

4.3.2 云原生2.0(华为)

随着云原生技术的成熟和市场需求的升级,云计算的发展已步入新的阶段。云原生2.0时代已经到来
从技术角度看,以容器、微服务以及动态编排为代表的云原生技术蓬勃发展,成为赋能业务创新的重要推动力,并已经应用到企业核心业务。从市场角度看,云原生技术已在金融、制造、互联网等多个行业得到广泛验证,支持的业务场景也愈加丰富,行业生态日渐繁荣。
云原生2.0,企业云化从“ON Cloud”走向“IN Cloud“,生于云、长于云且立而不破
企业新生能力基于云原生构建,使其生于云;应用、数据和AI的全生命周期云上完成,使其长于云;同时,既有能力通过立而不破的方式继承下来,并与新生能力有机协同。
智能升级新阶段,赋能“新云原生企业”
云原生2.0是企业智能升级的新阶段,企业云化从“ON Cloud”走向“IN Cloud“,成为”新云原生企业“。新生能力与既有能力立而不破、有机协同,实现资源高效、应用敏捷、业务智能,安全可信。
云原生 IN 基础设施
华为云基于“云原生 IN 基础设施”的理念,打造了以应用为中心的云原生基础设施。
目前,华为云云原生基础设施包含了云容器引擎CCE、云容器实例CCI、容器镜像服务SWR、智能边缘平台IEF、多云容器平台MCP、应用编排服务AOS等8大核心容器产品,并以此为基础构建了云原生裸金属、云原生高性能计算、云原生混合云、云原生边缘计算四大解决方案,满足企业业务智能升级过程中,对高性能基础设施、分布式业务架构、完善的云原生应用生态的诉求。

4.3.3 未来发展趋势

1、运维继续下沉,服务网格将成为主流,Serverless逐步推广
云计算的一个发展方向就是运维下沉,将和业务无关的管理功能和运维工作尽量下沉到基础设施中,应用可以聚焦在业务能力的开发和运营。这个趋势演化的过程,影响了云计算的发展方向。从一开始的虚拟化,到IaaS,到PaaS都是将应用系统的部分运维职责交给平台运维的过程。
2、软硬结合,解决虚拟化性能问题的利器
随着云计算的发展,虚拟化技术越来越多的被使用,从计算虚拟化到存储虚拟化到网络虚拟化。虚拟化技术带来了很多的好处,虚拟化是基础设施服务化的基础,通过虚拟化,可以实现基础设施即代码,大大提升了资源的可管理性和自动化程度。但是虚拟化带来了另外一个问题,就是性能的损耗和软件进程之间的相互影响问题。
为了解决这两个问题,目前一个解决思路就是软硬结合,讲云平台的管理进程,如调度管理,网络的虚拟交换机,存储的虚拟存储网关从操作系统进程中剥离出来,让这些进程跑在专门设计的服务器板卡上,这些板卡专门设计的,通常含有定制化的芯片(FPGA),可以进行编程,从而可以保持虚拟化话的优势的同时,使的管理进程和业务进程隔离,避免相互影响;同时由于通过定制芯片(如FPGA)来处理,性能会有很大提升,大大降低了虚拟化的损耗。
3、容器虚拟机进一步融合
容器和虚拟机的优势和劣势,从容器技术诞生的那天起就一直在争论。容器轻量化,良好的封装能力和部署简便的特点,特别是在Kubernetes出现后,大有取代虚拟机的气势。但是在处理重应用(如关系型数据库,大数据等)的这点上,容器技术显得有些力不从心。在这种情况下,如何实现容器技术和虚拟化技术的融合,发挥两者的长处,成为云计算的一个发展课题。目前的技术主要有三种,一种是容器虚拟机的混布;一种是轻量级虚拟机;最后是安全容器。
4、运维:可编程的Linux内核
由于引入了可扩展的Berkeley数据包过滤器(Extended Berkeley Packet Filter,eBPF),Linux内核在使用方式上有了重大变化
5、开发:Rust逐渐替代C++
数十年来,我们的操作系统和其他重要的基础架构软件一直使用C或C ++编写,但是,如今,越来越多的系统架构师得出的结论是,由于用不安全的方式来处理内存和其他因素,要完全安全地保护用这些语言编写的程序从根本上来说是困难的。
所以最近,越来越多的拥护者选择了新的语言Rust,它不仅具有C/C ++的速度,而且还具有编写安全的应用程序所必需的组件。在2020年的AllThingsOpen虚拟会议上,微软云开发倡导者Ryan Levick 解释了为什么Microsoft逐渐改用Rust来构建其基础结构软件,而不再使用C/C ++。并鼓励其他软件行业巨头也考虑相同的问题。

5 参考链接

  1. https://developer.aliyun.com/article/722745
  2. https://zhuanlan.zhihu.com/p/150190166
  3. https://zhuanlan.zhihu.com/p/390567373
  4. https://support.huaweicloud.com/productdesc-cce/cce_productdesc_0009.html
  5. https://landscape.cncf.io/
  6. https://developer.aliyun.com/article/722745
  7. https://zhuanlan.zhihu.com/p/149658062
  8. https://cloud.tencent.com/developer/article/1851558

NewSQL主流数据库对比——GIS篇

NewSQL主流数据库对比——GIS篇

[TOC]

NewSQL 简介

NewSQL 是一种新方式关系数据库,意在整合 RDBMS 所提供的ACID事务特性,以及 NoSQL 提供的横向可扩展性。

大多数 NewSQL 数据库做了全新的设计,或是主要聚焦于 OLTP,或是采用了 OLTP/OLAP 的混合架构载的全新设计。

  • 一致性:相对于可用性而言,NewSQL 更重视一致性,即侧重 CAP 中的 C 和 P。
  • 内存数据库:一些 NewSQL 解决方案使用内存(RAM)作为存储介质。
  • HTAP:HTAP(混合事务 / 分析处理,Hybrid Transactional/Analytical Processing)

1166059074

!!!对比分析

NewSQL 开发商 是否开源 特性 分布式 空间支持 社区活跃度 拓展
Spanner 谷歌 × 存储的数据是Key-value 水平扩展 × Google社区
OceanBase 阿里 开源 MySQL/ORACLE兼容、增量数据放在内存、基线数据放在SSD盘、读写分 线性扩展 × 阿里社区、博客
TDSQL 腾讯 × 提供两种版本MySQL和PostgreSQL 水平扩展 PostGIS 腾讯社区
Azure Cosmos DB 微软 商业(2RMB/月) 支持键值、列存储、文档和图数据库,并支持通过 SQL 和 NoSQL API 提供数据。 水平扩展 支持 微软社区
TiDB PingCAP 开源 兼容 MySQL、TiDB、TiKV计算层与存储层的分离解耦架构 水平扩展 × 社区、博客 TiSpark
SequoiaDB巨杉数据库 巨杉数据库 开源 国产、金融级分布式关系型数据库 集群 × 社区、博客
CockroachDB蟑螂数据库 蟑螂实验室 商业来源许可证(BSL) 支持PostgreSQL、去中心化架构、支持kuberneter编排 水平扩展 部分兼容PostGIS Cockroach Labs 博客
Citus 微软并购 GPL v3 开源 PostgreSQL 扩展、并行处理、微软云、简单易用 支持分布式 PostgreSQL PostGIS 论坛、博客
ClustrixDB MariaDB 2018年收购 收费 类MYSQL 分布式
MemSQL/SingleStore 前 Facebook 工程师创办 开源的社区版 内存数据库、兼容MySQL、MySQL InnoDB 分布式 地理空间和全文搜索 image-20210704154249267社区论坛
VoltDB Postgres和Ingres联合 GPLv3、AGPL 内存数据库、采用类MPP架构、并行的单线程处理方式确保数据一致性 水平扩展 仅支持简单空间查询 博客
Vitess CNCF 开源 部署,扩展和管理MySQL实例、支持kuberneter编排 无限水平扩展 MySQL Spatial 博客、社区
NuoDB NuoDB × 去集中化、唯一一个具有专利的、弹性可伸缩的SQL关系数据库 文档都是视频
Altibase Hybrid DBMS
YugabyteDB
GenieDB
ScaleDB

Azure Cosmos DB

Azure Cosmos DB 是一种用于现代应用开发的完全托管式 NoSQL 数据库服务。 获得有保证的个位数毫秒级响应时间和 由 SLA 支持的 99.999% 可用性、 自动、即时的可伸缩性 ,以及用于 MongoDB 和 Cassandra 的开放源代码 API。

使用 Azure Synapse Link for Azure Cosmos DB 通过非 ETL 分析从实时数据中获取见解。

NoSQL 数据库与关系数据库之间的差别

  • SQL API
  • Cassandra API
    • Azure Cosmos DB Cassandra API 可以充当为 Apache Cassandra 编写的应用的列族数据存储
    • 通过 Cassandra API 可以使用 Cassandra 查询语言 (CQL)、基于 Cassandra 的工具(如 cqlsh)和熟悉的 Cassandra 客户端驱动程序与 Azure Cosmos DB 中存储的数据进行交互。
  • Gremlin API
    • Azure Cosmos DB 通过 Gremlin API 在为任何规模设计的完全托管数据库服务中提供图形数据库服务。
    • 使用 Gremlin 查询语言。
  • 表 API
    • 类似于CloudTable。
    • Azure Table Storage 提供的表 API 适用于为 Azure 表存储
    • 表 API 包含可用于 .NET、Java、Python 和 Node.js 的客户端 SDK。
  • Azure Cosmos DB API for MongoDB
    • 通过用于 MongoDB 的 Azure Cosmos DB API,可以轻松使用 Cosmos DB,就像它是 MongoDB 文档数据库一样。

Azure Cosmos DB 中的地理空间和 GeoJSON 位置数据

Altibase(商用收费)

CockroachDB

CockroachDB(https://www.cockroachlabs.com)是Google备受瞩目的Spanner的开源模仿,承诺提供一种高存活性、强一致性,可横向扩展的SQL数据库。主要的设计目标是全球一致性和可靠性,从蟑螂(cockroach)的命名上是就能看出这点 [ 打不死的小强:) ]。Cockroach节点是均衡的,其设计目标是同质部署(只有一个二进制包)且最小配置。CockroachDB的扩展非常容易,只要一行命令,秒级进行。

  • PostgreSQL生态中的很多工具、程序和应用能够适用于CockroachDB(不用修改或少量修改)。
  • 虽然CockroachDB支持PostgreSQL语法和驱动程序,但它不是完全兼容PostgreSQL。
  • 2020年的20.2版本开始支持geospatial,但未完全兼容PostGIS。
  • 采用分层架构,最顶层是SQL层。SQL层构建于分布式KV存储之上

img

https://blog.csdn.net/qq_34924156/article/details/89236693

Citus

CitusDB采用PostgreSQL的插件形式(not a fork),即享受PostgreSQL的强大支持,又同时拥有分布式数据库能力

CitusDB 与 HDFS 的分布式非常相似,在 Master 上存储元数据,Work 节点存储分片,同时 1 个分片至少要存储在 2 个 Work 节点(可配置更多)上保障其可用性。

image-20210703221436470

  • 苏宁citus分布式数据库应用实践

https://itdks.su.bcebos.com/844d50a9b73f4d8283d746cbff23b5e9.pdf

  • 利用 citus 支持地理大数据。全世界的点状POI,数据量级已达亿级别,存储在单一的PostgreSQL数据表中,若查询涉及到全表扫描,性能会差很多

https://www.giserdqy.com/secdev/openlayers/21870/

  • Citus是一个PostgreSQL的扩展,类似于PostGIS、pgRouting,它主要的作用是将一张大表进行水平分表,每个分表称为“分片(Shard)”,按照一定的规则分布(?)到多台机器上,并且将查询分布到不同节点并行执行,最后汇总结果。

img

ClustrixDB(收费)

MemSQL

由前 Facebook 工程师创办的 MemSQL,号称世界上最快的分布式关系型数据库,兼容 MySQL 但快30倍,能实现每秒 150 万次事务。原理是仅用内存并将 SQL 预编译为C++。

  • MemSQL is Now SingleStore
  • 2018年,MemSQL 搞了个炸裂消息,宣布 MemSQL 6.7 可以免费用于生产环境,没有功能限制,磁盘容量无限制,只限制整个 MemSQL 集群总内存使用不得超过 128 GB,如果做双机备份,那么就是每台机器 64 GB,已经很够用了。
  • 使用ACID事务每秒接收数百万个事件,同时以关系SQL、JSON、地理空间和全文搜索格式分析数十亿行数据。

image-20210704150733553

  • 地理空间支持

    • SingleStore使用了一个类似于googleearth的球形模型。它假设一个半径为6367444.66米的完美球形地球。

    • 地理数据类型

      image-20210704151619023

    • 空间查询

      image-20210704151741992

    • GEOJSON。SingleStore 没有本机 GeoJSON 支持。但是,我们有通用的 JSON 支持以及计算列。结合这些功能,您可以使用 SingleStore 的内置 JSON 类型和 GEOGRAPHYPOINT 类型导入 GeoJSON 数据的子集。

      image-20210704151949000

  • 社区网址:https://www.singlestore.com/community-hub/

  • 资源手册:https://www.singlestore.com/resources/

NuoDB

NuoDB 是newsql数据库的典型代表之一,同时也是世界上首个也是唯一一个具有专利的、弹性可伸缩的SQL关系数据库,主要用于去集中化的计算资源。

OceanBase

OceanBase 社区版是一款开源分布式 HTAP(Hybrid Transactional/Analytical Processing)数据库管理系统,具有原生分布式架构,支持金融级高可用、透明水平扩展、分布式事务、多租户和语法兼容等企业级特性。

2020 年 5 月,OceanBase 以 7.07亿 tpmC 的在线事务处理性能,打破了 OceanBase 自己在 2019 年创造的 6088万 tpmC 的 TPC-C 世界纪录。截止至目前,OceanBase是第一个也是唯一一个上榜的中国数据库。

image-20210704164251964

  • 版本选择

    image-20210704164312198

  • 产品优势

    • 高性能:OceanBase采用了读写分离的架构,把数据分为基线数据和增量数据。其中增量数据放在内存里(MemTable),基线数据放在SSD盘(SSTable)。对数据的修改都是增量数据,只写内存。所以DML是完全的内存操作,性能非常高。
    • 低成本:OceanBase通过数据编码压缩技术实现高压缩。数据编码是基于数据库关系表中不同字段的值域和类型信息,所产生的一系列的编码方式,它比通用的压缩算法更懂数据,从而能够实现更高的压缩效率。
    • 高兼容:兼容常用MySQL/ORACLE功能及MySQL/ORACLE前后台协议,业务零修改或少量修改即可从MySQL/ORACLE迁移至OceanBase。
    • 高可用:数据采用多副本存储,少数副本故障不影响数据可用性。通过“三地五中心”部署实现城市级故障自动无损容灾。
  • 定价

    image-20210704164141515

  • 社区:https://open.oceanbase.com/community/contribution

  • 博客:https://open.oceanbase.com/articles

  • 文档:https://open.oceanbase.com/docs

SequoiaDB

TDSQL

分布式数据库(Tencent Distributed SQL,以下简称 TDSQL)是腾讯打造的一款企业级数据库产品,具备强一致高可用、全球部署架构、高 SQL 兼容度、分布式水平扩展、高性能、完整的分布式事务支持、企业级安全等特性,同时提供智能 DBA、自动化运营、监控告警等配套设施,为客户提供完整的分布式数据库解决方案。

稳定、安全、高性能的分布式数据库,兼容 PostgreSQL 和 MySQL。

  1. MySQL 版

    高度兼容 MySQL,支持水平拆分(分表)的高性能数据库。部署在腾讯云上的一种支持自动水平拆分、Shared Nothing 架构的分布式数据库。TDSQL MySQL版 默认部署主备架构,提供容灾、备份、恢复、监控、迁移等全套解决方案,适用于 TB 或 PB 级的海量数据库场景。

  2. PostgreSQL 版 (原 TBase)

    稳定、安全、高性能的分布式数据库服务,满足您海量 HTAP 场景。集高扩展性、高SQL兼容度、完整的分布式事务支持、多级容灾能力以及多维度资源隔离等能力于一身。TDSQL PostgreSQL 版采用无共享的集群架构,为用户提供容灾、备份、恢复、监控、安全、审计等全套解决方案,适用于GB~PB级的海量 HTAP 场景。

    具有丰富的周边生态:

    • 支持强大的地理信息系统(GIS)。通过集群化的 PostGis 插件,支持存储空间地理数据,使 TDSQL PostgreSQL版 成为一个空间数据库,能够通过 SQL 语言高效的进行空间数据管理、数量测量和几何拓扑分析。
    • TDSQL PostgreSQL版 不仅是一个分布式关系型数据库系统,同时还支持非关系数据类型 JSON。
    • 支持 Foreign Data Wrappers(FDW)功能,该功能实现了部分的 SQL/MED 规定,允许用户使用普通 SQL 查询来访问位于 PostgreSQL 之外的数据。
      FDW 功能提供一套编程接口,用户可进行插件式的二次开发,建立外部数据源和数据库间的数据通道。大多数情况下用户可用 oracle_fdw、mysql_fdw、postgres_fdw,非关系型数据库的 redis_fdw、mongodb_fdw,以及大数据的 hive_fdw、hdfs_fdw 等。基于 FDW 功能和已有插件,TDSQL PostgreSQL版 提供强大的数据库联邦能力,通过 TDSQL PostgreSQL版 能够访问已有的多个数据源的数据。

image-20210704180213145

  • 价格

    image-20210704180327148

TiDB

VoltDB

Vitess

image-20210703213038339

  • Vitess包括使用与本机查询协议兼容的JDBC和Go数据库驱动。自2011年以来,Vitess一直为YouTube所有的数据库提供服务,现在已被许多企业采用并应用于实际生产。

  • Vitess是一个用于部署、扩展和管理大型MySQL实例集群的数据库解决方案。Vitess集Mysql数据库的很多重要特性和NoSQL数据库的可扩展性于一体。它的架构设计使得您可以像在物理机上一样在公共云或私有云架构中有效运行。它结合并扩展了许多重要的MySQL功能,同时兼具NoSQL数据库的可扩展性。

  • 一种云原生技术 Cloud-native

    • 容器化(Containerized):每个部分(应用程序,进程等)都封装在自己的容器中。这有助于重复性,透明度和资源隔离。
    • 动态编排(Dynamically orchestrated):动态的调度和管理容器以优化资源利用。
    • 面向微服务(Microservices oriented):应用程序基于微服务架构,显著提高架构演进的灵活性和可维护性。

CNCF案例研究:京东如何使用Vitess管理超大规模数据库

https://cloud.tencent.com/developer/article/1548244

NewSQL,NoSQL与OldSQL的混合部署

  • OldSQL+NewSQL
  • OldSQL+NoSQL
  • NewSQL+NoSQL

https://supmarket.net/xy/771126462


postgis 相关调研

基本概念

相关标准化组织机构介绍

  • OGC 表示开放地理空间信息联盟 (Open Geospatial Consortium-OGC) ,致力于提供地理信息行业软件和数据及服务的标准化工作。OGC在1994年到2004年期间机构名为Open GIS Consortium, 后因业务需要更名。

  • IS0/TC 211(国际标准化组织地理信息技术委员会211)

  • W3C(World Wide Web onsortium,万维网联盟)

  • OpenGIS(Open Geodata Interoperation Specification,开放地理数据互操作规范)

  • GML(Geographic Markup Language)由OGC定义的XML(标准通用标记语言的子集)格式,用来表达地理信息要素。

OpenGIS

要素(Feature):几何信息和属性信息

  • OpenGIS定义了一组基于数据的服务,而数据的基础是要素(Feature)。所谓要素,简单地说就是一个独立的对象,在地图中可能表现为一个多边形建筑物,在数据库中即是一个独立的条目。要素具有两个必要的组成部分——几何信息和属性信息。

几何信息:点(Point)、边缘(LineString)、面(Polygon)和几何集合(GeometryCollection)

  • OpenGIS将几何信息分为点、边缘、面和几何集合四种: 其中这里熟悉的线(LineString)属于边缘的一个子类,而多边形(Polygon)是面的一个子类。也就是说OpenGIS定义的几何类型并不仅仅是我们常见的点、线、多边形三种,它提供了更复杂更详细的定义,增强了未来的可扩展性。另外,几何类型的设计中采用了组合模式(Composite),将几何集合(GeometryCollection)也定义为一种几何类型。类似地,要素集合(FeatureCollection)也是一种要素。

属性信息(FeatureType):

  • 属性信息没有做太多的限制,可以在实际应用中结合具体的实现进行设置。相同的几何类型、属性类型的组合成为要素类型(FeatureType),类型相同的要素可以存放在一个数据源中。而一个数据源只能拥有一个要素类型。因此,可以用要素类型来描述一组属性相似的要素。

在面向对象的模型中,完全可以把要素类型理解为一个类,而要素则是类的实例。通过GIS中间件可以从数据源中取出数据,供WMS服务器和WFS服务器使用。WMS服务器接收请求,根据请求内容的不同,可以返回不同格式的最终数据。例如,WMS可以返回常用图片格式的地图片段供最终用户阅读(类似GoogleMaps),其中地图是根据一个样式文件(SLD)生成的,它描述了地图的线的宽度、色彩等;WMS也可以返回GeoRSS和KML用来与其他地图服务互通。WFS服务器也可以接收请求,但WFS将返回GML格式的地理信息数据。GML是一种基于XML的数据格式,它可以完整地再现数据,也是OpenGIS数据源的重要形式。也就是说,WFS返回的GML可以继续作为数据源。在WFS请求中,OpenGIS定义了一个Filter标准,用来实现对数据的筛选,使WFS更加灵活。另一方面,WFS还支持通过WFS-t提交客户端对数据的修改。通俗地说,WMS是只读的,而WFS则是可以读写的。

OGC地图服务标准介绍

OGC1999年开始 WMT1(Web Map Tested)和 WMT2 互操作项目。其中著名的GML来自WMT1的成果。在WMT2中OGC定义了三种地理参考信息模型:Web Map Server(WMS) , Web Feature Server(WFS) ,Web Coverage Server(WCS) .

OGC 地图服务协议,包括 WMS、WFS、WCS、WMTS、WPS 。其中比较重要的现在用得比较多的标准是GML、WMS和WFS。

网络地图服务(WMS)

  • Web Map Server(WMS)能够根据用户的请求返回相应的地图(包括PNG,GIF,JPEG等栅格形式或者是SVG和WEB CGM等矢量形式)。

  • WMS支持网络协议HTTP,所支持的操作是由URL定义的。有三个重要操作 GetCapabilitiesGetMapGetFeatureinfo

网络要素服务(WFS)

  • Web 要素服务(WFS)支持对地理要素的插入,更新,删除,检索和发现服务。该服务根据HTTP客户请求返回GML数据。

  • 其基础接口是:GetCapabilities,DescribeFeatureType,GetFeature  GetCapabilities同上。

网络覆盖服务(WCS)

  • Web地理覆盖服务(WCS):提供的是包含了地理位置信息或属性的空间栅格图层,而不是静态地图的访问。

  • 根据HTTP客户端要求发送相应数据,包括影像,多光谱影像和其它科学数据. 有二个重要操作GetCapabilities,GetCoverage GetCapabilities返回一个描述服务和XML文档,从中可获取覆盖的数据集合。

切片地图服务(TMS)

  • 切片地图服务(TMS)定义了一些操作,这些操作允许用户访问切片地图。WMTS可能是OGC首个支持RESTful访问的服务标准。

WMS和WNTS区别

  • WMTS服务和WMS服务对客户端请求服务的响应不同,比如在接受客户端请求WMTS服务时,返回给客户端是固定大小的瓦片,客户端根据索引号来获取每一张瓦片,而后拼接成地图进行展示,如图1所示;由于瓦片的规则是固定的,服务端可以预先缓存对应的瓦片,客户端需要时直接返回即可,因而WMTS是可缓存的。

img

END